Homelable could only own the root of an origin. Behind an existing proxy at `https://home.example/homelab/` the built `index.html` still asked for `/assets/*`, which fell through to whatever owned the root — and when that answered `text/html` for a `<script>` under `nosniff`, the browser failed the load on an HTTP 200. The only workaround was patching the checkout and rebuilding on every upgrade. One build-time knob, `VITE_BASE_PATH`, becomes Vite's `base`. Vite rewrites the asset URLs it emits; everything the app builds by hand goes through the new `utils/basePath.ts` — the axios instances, the WebSocket URL, the live-view route, the local brand icons, and the OIDC login href. `resolveServerPath` covers what the *backend* hands back, which is always root-absolute because it cannot know where the SPA is mounted: uploaded floor-plan URLs already stored in a canvas are resolved at render time, so plans predating the move keep loading. The OIDC callback used to redirect to `/`, dropping subpath users at the origin root after login; it now reads the prefix back out of `OIDC_REDIRECT_URI`. The default is `/`, and stays a no-op there by construction: every helper returns the string it returned before, the root build output is unchanged and both nginx sites are byte-identical to what shipped — the Docker image copies `docker/nginx.conf` verbatim and the installer keeps its original heredoc. Only a non-default prefix takes the generated config. Those generated configs use `root`, never `alias`, since `alias` plus `try_files` mis-resolves `$uri` — the Docker build lands the bundle in the matching subdirectory, and the installer symlinks it under `/var/www/homelable`. Both accept either reverse-proxy style, prefix forwarded intact or already stripped, with no redirect loop between them, and `absolute_redirect off` stops the no-slash 301 from eating the port. Closes #334. ha-relevant: maybe
114 lines
5.6 KiB
Bash
114 lines
5.6 KiB
Bash
# Backend - server-side only (NEVER commit .env)
|
||
# Generate: python3 -c "import secrets; print(secrets.token_hex(32))"
|
||
SECRET_KEY=change_me_in_production
|
||
SQLITE_PATH=./data/homelab.db
|
||
# Uploaded media (floor plans) folder. Optional — defaults to <SQLITE_PATH dir>/uploads,
|
||
# which sits on the same persistent volume as the DB.
|
||
# UPLOAD_DIR=./data/uploads
|
||
# Set this to the URL(s) you use to access Homelable in your browser.
|
||
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]
|
||
|
||
# Serve the UI under a subpath instead of the root of the origin, e.g. /homelab/.
|
||
# Read by docker-compose at *build* time (docker compose build frontend), since the
|
||
# prefix is baked into the frontend bundle. See INSTALLATION.md.
|
||
# VITE_BASE_PATH=/homelab/
|
||
|
||
# Auth — local mode is the backward-compatible default.
|
||
AUTH_MODE=local
|
||
# Local credentials: admin / admin by default.
|
||
# ⚠️ Change before exposing on a network.
|
||
# Generate a new hash: python3 -c "import bcrypt; print(bcrypt.hashpw(b'yourpassword', bcrypt.gensalt()).decode())"
|
||
# ⚠️ Keep the single quotes around the hash — bcrypt hashes contain \$ which Docker misinterprets without them.
|
||
AUTH_USERNAME=admin
|
||
AUTH_PASSWORD_HASH='$2b$12$RtMbyw17l4N5UGzeXMNAWuzCaVV.XFBY7ZetWheQhxcBDcxahapkG'
|
||
|
||
# OpenID Connect (optional alternative to local auth).
|
||
# Set AUTH_MODE=oidc and configure every value below. The callback must be
|
||
# registered exactly at the identity provider. Keep CORS_ORIGINS restricted to
|
||
# the Homelable browser origin; wildcard CORS is rejected in OIDC mode. OIDC
|
||
# also requires SECRET_KEY to contain at least 32 bytes.
|
||
# AUTH_MODE=oidc
|
||
# OIDC_DISCOVERY_URL=https://idp.example/application/o/homelable/.well-known/openid-configuration
|
||
# OIDC_CLIENT_ID=homelable
|
||
# OIDC_CLIENT_SECRET=replace-with-a-secret
|
||
# OIDC_REDIRECT_URI=https://homelable.example/api/v1/auth/oidc/callback
|
||
# OIDC_SCOPES="openid profile email"
|
||
# OIDC_COOKIE_SECURE=true
|
||
# OIDC_SESSION_EXPIRE_MINUTES=480
|
||
# OIDC_TRANSACTION_EXPIRE_SECONDS=600
|
||
|
||
# Scanner — JSON array of CIDR ranges to scan
|
||
SCANNER_RANGES=["192.168.1.0/24"]
|
||
|
||
# Deep scan (optional) — extra nmap port ranges + HTTP probe for service ID on
|
||
# custom ports. Defaults below are overridable per-scan from the scan dialog.
|
||
# SCANNER_HTTP_RANGES: JSON array of port specs, each a single port "N" or an
|
||
# inclusive range "N-M" (1–65535, N <= M). Not CIDRs, not bare ints.
|
||
# Example: SCANNER_HTTP_RANGES=["8080","9000-9100"]
|
||
SCANNER_HTTP_RANGES=[]
|
||
SCANNER_HTTP_PROBE_ENABLED=false
|
||
SCANNER_HTTP_VERIFY_TLS=false
|
||
|
||
# Status checker interval in seconds
|
||
STATUS_CHECKER_INTERVAL=60
|
||
|
||
# MCP server — used by the mcp service (port 8001)
|
||
# MCP_API_KEY: authenticates AI clients (Claude Code, etc.) → MCP server
|
||
# MCP_SERVICE_KEY: authenticates MCP server → backend (never exposed externally)
|
||
# Generate keys: python3 -c "import secrets; print(secrets.token_hex(32))"
|
||
MCP_API_KEY=mcp_sk_changeme
|
||
MCP_SERVICE_KEY=svc_changeme
|
||
|
||
# Live view — read-only public canvas at /view?key=<value>
|
||
# Off by default. Set to a random secret to enable.
|
||
# Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
||
# LIVEVIEW_KEY=
|
||
|
||
# Gethomepage widget — read-only stats at /api/v1/stats/summary
|
||
# Off by default. Set to a random secret to enable; clients must send
|
||
# the same value in the `X-API-Key` header.
|
||
# Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
||
# HOMEPAGE_API_KEY=
|
||
|
||
# Proxmox VE import — pull hosts/VMs/LXC from the Proxmox REST API.
|
||
# The token is a credential: kept in memory only, never written to disk by the
|
||
# app, never returned by any API. Create it under Datacenter → Permissions →
|
||
# API Tokens and grant the read-only PVEAuditor role at path "/".
|
||
# Token id format: user@realm!tokenname (e.g. root@pam!homelable).
|
||
# Required only for auto-sync; one-off imports can pass the token in the dialog.
|
||
# PROXMOX_TOKEN_ID=root@pam!homelable
|
||
# PROXMOX_TOKEN_SECRET=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||
# PROXMOX_HOST=192.168.1.10
|
||
# PROXMOX_PORT=8006
|
||
# PROXMOX_VERIFY_TLS=true # set false only for self-signed certs
|
||
|
||
# Zigbee2MQTT auto-sync — pull the mesh from an MQTT broker on a schedule.
|
||
# MQTT credentials are secrets: kept in memory only, never written to disk by
|
||
# the app, never returned by any API. Manual imports (the Zigbee dialog) are
|
||
# unaffected — this block only powers Settings → Zigbee auto-sync and /sync-now.
|
||
# Required only for auto-sync; one-off imports pass their config in the dialog.
|
||
# ZIGBEE_MQTT_HOST=192.168.1.20
|
||
# ZIGBEE_MQTT_PORT=1883
|
||
# ZIGBEE_MQTT_USERNAME=mqttuser # optional
|
||
# ZIGBEE_MQTT_PASSWORD=mqttpass # optional
|
||
# ZIGBEE_BASE_TOPIC=zigbee2mqtt
|
||
# ZIGBEE_MQTT_TLS=false # true for TLS brokers (typically port 8883)
|
||
# ZIGBEE_MQTT_TLS_INSECURE=false # skip cert verify (self-signed only; requires TLS)
|
||
# Seconds to wait for the Z2M bridge to answer a networkmap request. Applies to
|
||
# manual imports too. Raise it on a large mesh (200+ devices can take minutes)
|
||
# if an import fails with "Timed out waiting for networkmap response".
|
||
# ZIGBEE_NETWORKMAP_TIMEOUT=300
|
||
|
||
# Z-Wave JS UI (zwavejs2mqtt) auto-sync — same MQTT secret/env rules as Zigbee.
|
||
# ZWAVE_MQTT_HOST=192.168.1.20
|
||
# ZWAVE_MQTT_PORT=1883
|
||
# ZWAVE_MQTT_USERNAME=mqttuser # optional
|
||
# ZWAVE_MQTT_PASSWORD=mqttpass # optional
|
||
# ZWAVE_PREFIX=zwave
|
||
# ZWAVE_GATEWAY_NAME=zwavejs2mqtt
|
||
# ZWAVE_MQTT_TLS=false # true for TLS brokers (typically port 8883)
|
||
# ZWAVE_MQTT_TLS_INSECURE=false # skip cert verify (self-signed only; requires TLS)
|
||
# Seconds to wait for the gateway to answer an MQTT request. Same rationale as
|
||
# ZIGBEE_NETWORKMAP_TIMEOUT above.
|
||
# MQTT_RESPONSE_TIMEOUT=300
|