# SvxPortal web — installation HOWTO This is the **web front-end only** for a SvxReflector: node map, monitor, last-heard, talkgroup list, user accounts and the reflector admin pages. It is plain HTML/JavaScript plus a small PHP backend that stores accounts in Redis. No build step, no framework, no composer. What you need on the server: | Component | Why | |-----------|-----| | nginx | serves the static files and passes `*.php` to PHP-FPM | | PHP 8.2+ with FPM | the `auth/` backend (login, accounts, admin API) | | PHP extensions: `redis`, `mbstring`, `openssl`, `json`, `session` | used by `auth/lib.php` | | Redis 6+ | stores accounts, sessions, bans, talkgroup requests | | A way to send e-mail | signup verification and password reset (local MTA **or** an SMTP relay, see step 5) | | TLS certificate | the session cookie is `Secure`; login does not work over plain HTTP | Everything below assumes Debian 12/13 or Ubuntu 24.04, PHP 8.4 and that the site will live at `https://svxportal.example.com`. Replace the host name everywhere you see it. --- ## 1. Packages ```bash sudo apt install nginx php8.4-fpm php8.4-redis php8.4-mbstring php8.4-cli redis-server certbot ``` Check the PHP version you actually got (`php -v`) and use that number instead of `8.4` in the file names below. ## 2. Copy the web files ```bash sudo mkdir -p /var/www/html/svxportal sudo rsync -a --exclude .git --exclude install --exclude INSTALL.md ./ /var/www/html/svxportal/ sudo chown -R www-data:www-data /var/www/html/svxportal sudo find /var/www/html/svxportal -type d -exec chmod 2775 {} + sudo find /var/www/html/svxportal -type f -exec chmod 0644 {} + ``` ## 3. Redis (dedicated instance, unix socket) ```bash sudo cp install/redis-svxportal.conf /etc/redis/redis-svxportal.conf sudo mkdir -p /var/lib/redis-svxportal sudo chown redis:redis /var/lib/redis-svxportal /etc/redis/redis-svxportal.conf sudo chmod 640 /etc/redis/redis-svxportal.conf ``` Generate a password and put it in the config: ```bash openssl rand -hex 32 sudo sed -i "s/^requirepass .*/requirepass PASTE-THE-VALUE-HERE/" /etc/redis/redis-svxportal.conf sudo systemctl enable --now redis-server@svxportal ``` The socket is created with mode 770 and group `redis`, so the web user must be in that group: ```bash sudo usermod -aG redis www-data ``` Test: ```bash redis-cli -s /run/redis-svxportal/redis.sock -a 'PASTE-THE-VALUE-HERE' ping # -> PONG ``` ## 4. PHP-FPM Two pools are used. The default pool (`www`) handles all normal pages. A second small pool handles only `auth/admin_reflector_api.php`, because that script calls `exec()` to reach the sudo helpers (step 9) and you do not want `exec()` enabled for the whole site. ```bash sudo cp install/php-fpm-svxadmin.conf /etc/php/8.4/fpm/pool.d/svxadmin.conf ``` In the default pool hardening is recommended. Edit `/etc/php/8.4/fpm/pool.d/www.conf` and add at the end: ```ini php_admin_value[disable_functions] = exec,passthru,shell_exec,system,popen,proc_open,proc_close,pcntl_exec,dl,phpinfo,show_source php_admin_value[expose_php] = Off ``` Then: ```bash sudo systemctl restart php8.4-fpm ls -l /run/php/ # both php8.4-fpm.sock and php8.4-fpm-svxadmin.sock must exist ``` ## 5. Web app config (`auth/config.php`) ```bash cd /var/www/html/svxportal sudo -u www-data cp auth/config.php.example auth/config.php sudo chmod 640 auth/config.php sudoedit auth/config.php ``` Set: - `redis.unix_socket` -> `/run/redis-svxportal/redis.sock` - `redis.password` -> the value from step 3 - `base_url` -> `https://svxportal.example.com` (used in the e-mail links) - `mail.from` -> the sender address for verification / reset mails E-mail: with only `mail.from` set, PHP's `mail()` is used, which needs a working local MTA (`/usr/sbin/sendmail`, e.g. `msmtp-mta` or `postfix`). If you do not have one, add an SMTP relay instead and no MTA is needed: ```php 'mail' => [ 'from' => 'no-reply@example.com', 'from_name' => 'SvxPortal', 'host' => 'smtp.example.com', // SSL SMTP (implicit TLS) 'port' => 465, 'user' => 'smtp-user', 'pass' => 'smtp-password', ], ``` `config.php` is listed in `.gitignore` and is never served by nginx (all `*.php` go to PHP-FPM, which just executes `return [...]`). Keep it mode 640. ## 6. Talkgroup list file The talkgroup list is a JSON file the portal reads and writes: ```bash sudo cp install/tmpfiles-svxportal-tglist.conf /etc/tmpfiles.d/svxportal-tglist.conf sudo systemd-tmpfiles --create /etc/tmpfiles.d/svxportal-tglist.conf ``` This creates `/etc/svxlink/tglist.json` owned by `www-data`. The path is `tglist_file` in `config.php`; change both if you want it elsewhere. ## 7. nginx + TLS ```bash sudo mkdir -p /var/www/letsencrypt sudo cp install/nginx-svxportal.conf /etc/nginx/sites-available/svxportal sudo sed -i 's/svxportal\.example\.com/YOUR.HOST.NAME/g' /etc/nginx/sites-available/svxportal ``` Get a certificate first (the config references it): ```bash sudo certbot certonly --webroot -w /var/www/letsencrypt -d YOUR.HOST.NAME ``` If certbot has not created `/etc/letsencrypt/options-ssl-nginx.conf` and `ssl-dhparams.pem`, run `sudo certbot --nginx` once or remove those two lines. ```bash sudo ln -s /etc/nginx/sites-available/svxportal /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` Open `https://YOUR.HOST.NAME/` — the page should render. Click **Sign in** and you should get a proper "wrong password" style error, not a 500. A 500 here almost always means Redis (wrong socket/password, or `www-data` not in group `redis`). Also edit these files in the web root for your own site name: `robots.txt`, `sitemap.xml` and the ``/meta tags at the top of `index.html`. ## 8. First admin account Accounts normally self-register and verify by e-mail. The **first** admin is created from the command line with the bundled script; it reuses the portal's own library, so it writes exactly what `signup.php` would: ```bash cd /var/www/html/svxportal sudo -u www-data php svx-mkadmin.php ``` Answer the prompts (callsign, e-mail, password, admin level). Levels: | Level | Meaning | |-------|---------| | 1 | normal user | | 2 | admin: accounts, nodes, certificates, talkgroups | | 3 | super admin: may also see reflector/MQTT secrets and manage other admins | Log in on the web page with that callsign. The **Admin** menu appears. ## 9. Live data API — what this repository does NOT contain The dashboard (node list, map, monitor, last-heard, trunks) fetches JSON and Server-Sent Events from `/svxportal/api/...`, which nginx proxies to `127.0.0.1:8098`. That backend service is **not included** here. Without it the account/admin/talkgroup pages work, but the live node views stay empty (the page shows a connection error banner). Endpoints the UI expects: | Path | Used by | |------|---------| | `/svxportal/api/status` | node table, topology | | `/svxportal/api/stream` | live updates (SSE) | | `/svxportal/api/lastheard` | last-heard list | | `/svxportal/api/monitor-nodes` | monitor view | | `/svxportal/api/local-trunks` | trunks view | You need to provide something that serves these, for example a small service that reads your svxreflector's `HTTP_SRV_PORT` status endpoint and republishes it. The static reflector status page in `help.html` describes the fields. ## 10. Reflector admin features (optional, need sudo helpers) The pages **Admin → Reflector**, **Certificates**, **Node control** and **MQTT** talk to the running svxreflector on the same machine. They do it through small root-owned helper scripts called with `sudo` from `auth/admin_reflector_api.php`, `auth/node_cmd_api.php` and `auth/node_auth_api.php`: | Helper (expected in `/usr/local/sbin/`) | Purpose | |------------------------------------------|---------| | `svxreflector-pty` | writes `NODE BLOCK/UNBLOCK` and CA commands to the reflector's `COMMAND_PTY` | | `svxreflector-systemctl` | restart/reload the `svxreflector` service | | `svxreflector-users` | add/remove `[USERS]`/`[PASSWORDS]` entries in `svxreflector.conf` | | `svx-mqtt-provision` | create/remove per-node MQTT accounts | Those helpers and their `/etc/sudoers.d/` rules are **not part of this repository**; they are specific to how each reflector box is set up. Write your own for your environment (validate the callsign argument strictly, allow `www-data` to run only that one script with `NOPASSWD`), or skip this: all other pages work without them and the admin API returns a clear error when a helper is missing. The certificate pages additionally read `/etc/svxlink/svxreflector.conf` to find `CERT_PKI_DIR` and watch `pending_csrs/` in it, so `www-data` needs read access to the conf and the PKI directories (never to the private keys). `install/svxreflector.conf.example` shows a complete reflector config as used with this portal: PKI/CA paths, `COMMAND_PTY`, `HTTP_SRV_PORT`, the `[USERS]`/`[PASSWORDS]` layout the helper edits, MQTT, and trunk/satellite sections. All secrets and hostnames in it are placeholders. ## 11. Checklist when something does not work - **500 on any `auth/*.php`**: `sudo tail -f /var/log/nginx/svxportal-error.log` and `journalctl -u php8.4-fpm`. Usually Redis auth or missing `php-redis`. - **"Not connected" banner, empty node list**: no backend on `127.0.0.1:8098` (step 9). - **No verification e-mail**: `mail()` without an MTA silently fails. Configure the SMTP relay (step 5) and check `journalctl -u php8.4-fpm` for `smtp:` lines. - **Login works but is lost on next page**: site not on HTTPS (cookie is `Secure`) or `base_url` wrong. - **Admin API says "exec disabled"**: `admin_reflector_api.php` was served by the default pool. Check the nginx `location =` block and that `php8.4-fpm-svxadmin.sock` exists.