Files
shopdb-flask/.env.example
cproudlock af42ba3589
All checks were successful
CI / naming (push) Successful in 3s
CI / frontend (push) Successful in 13s
CI / migrations-mysql (push) Successful in 40s
CI / backend (push) Successful in 4m7s
Register the endpoints list without a trailing slash, and stop hiding its errors
An endpoint reported telemetry correctly for hours while the Fleet page showed
"No endpoints found". The PC detail page displayed its panels the whole time, so
ingest was plainly working; only the list was empty.

The list route was the one route in 331 paths registered with a trailing slash:

    @endpoints_bp.route('/', methods=['GET'])      ->  /api/endpoints/

The browser client calls it without one, so Flask answered a 308 redirect to an
ABSOLUTE url. On dev that resolves and nobody notices. Behind the IIS subpath it
cannot: with no ProxyFix, Werkzeug builds that Location from the scheme and host
it believes it has, so it emitted http:// and dropped the /shopdb prefix. A
browser will not follow an https page to http, the request failed, and the
page's catch block rendered the empty state.

Every other route in the plugin worked throughout, because none of them carries
a trailing slash - the detail panels call /endpoints/asset/<id>/status and were
never redirected. Detail works, list empty is the signature of this bug.

Registered as '' now, so the rule is /api/endpoints. That is what
CONTRIBUTING.md's API section spells and what every other list route in the
product already did. The inventory and the generated spec are updated to match.

Also here:

  - tests/test_core/test_no_redirecting_routes.py fails the build if any /api
    rule is registered with a trailing slash and strict slashes. Verified by
    planting the violation: it named the offending route, then passed once
    reverted. It takes the shared app fixture rather than building its own
    application - doing that rebinds SQLAlchemy and leaves the NEXT module
    without tables, which showed up as three unrelated "no such table: settings"
    failures visible only in a full run.

  - The Fleet page now renders "Could not load endpoints (HTTP nnn). This is a
    failed request, not an empty fleet." A list that reports a 403 and an empty
    fleet identically is what made this take as long as it did.

  - TRUSTED_PROXY_COUNT (default 0) applies ProxyFix when set. Off by default on
    purpose: it makes X-Forwarded-* trusted, and a client can send those itself,
    while the login rate limit and account lockout are keyed on the client
    address. Set it to 1 on an instance behind IIS.

Full suite 2124 passed, vitest 260 passed, naming and docs gates green.
2026-08-28 17:48:06 -04:00

125 lines
5.3 KiB
Plaintext

# shopdb-flask environment template.
#
# Copy to .env and fill in the values. .env is gitignored. ProductionConfig
# refuses to boot when SECRET_KEY, JWT_SECRET_KEY, DATABASE_URL, or
# CORS_ORIGINS are missing or use the dev defaults.
#
# See the DEPLOY wiki page for the full per-site deployment runbook.
# ---- Flask ----
FLASK_APP=wsgi.py
# Set to 'production' for live sites. Other valid values: 'development',
# 'testing'. Production triggers ProductionConfig.validate() at boot.
FLASK_ENV=production
# ---- Required secrets (production refuses to boot without these) ----
# Generate strong random values, e.g.:
# python -c "import secrets; print(secrets.token_urlsafe(64))"
SECRET_KEY=change-this-to-a-secure-random-string
JWT_SECRET_KEY=change-this-to-another-secure-random-string
# ---- Database (required) ----
# Format: mysql+pymysql://<user>:<password>@<host>:<port>/<database>?charset=utf8mb4
# In docker-compose, host is `db` (the service name).
# The ?charset=utf8mb4 keeps the connection on utf8mb4; create the database as
# utf8mb4 too (see the DEPLOY wiki page). Both must be utf8mb4 to match the schema.
DATABASE_URL=mysql+pymysql://shopdb:CHANGE_ME@db:3306/shopdb_flask?charset=utf8mb4
# ---- CORS (required, no wildcards in production) ----
# Comma-separated list of explicit origins permitted to call the API.
# Example for a single-host facility deploy:
# CORS_ORIGINS=https://shopdb.facility-a.example.com
# Wildcard '*' is rejected by ProductionConfig.validate().
CORS_ORIGINS=http://localhost:5173
# ---- Reverse proxy (optional, default 0 = no proxy) ----
# Set to the number of proxies actually in front of this instance - 1 when IIS
# or nginx serves it under a subpath such as /shopdb. Without it the app sees
# the proxy's connection instead of the browser's (scheme http, the proxy's
# host and IP), so any absolute URL it builds is wrong at the browser. That is
# what made a redirect on the endpoints list unfollowable behind IIS.
#
# Leave it at 0 when nothing fronts the app. It makes X-Forwarded-* trusted,
# and a client can send those headers itself - the login rate limit and the
# account lockout are keyed on the client address.
TRUSTED_PROXY_COUNT=0
# ---- JWT lifecycle (optional, defaults shown) ----
JWT_ACCESS_TOKEN_EXPIRES=3600
JWT_REFRESH_TOKEN_EXPIRES=2592000
# ---- Logging (optional) ----
LOG_LEVEL=INFO
# ---- docker-compose only ----
# These are read by docker-compose.yml; not used by the Flask app directly.
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD
MYSQL_PASSWORD=CHANGE_ME_APP_PASSWORD
MYSQL_PORT=3306
API_PORT=5001
# Air-gapped deploy only (docker-compose.airgap.yml): the loaded image tag,
# which MUST match what build-offline-bundle.ps1 -Version produced. Ignored by
# the connected build template (docker-compose.yml builds from source).
IMAGE_TAG=0.7.0
# ---- Zabbix integration (optional, for printer supply monitoring) ----
ZABBIX_URL=
ZABBIX_TOKEN=
# ---- Per-plugin collector API keys (optional) ----
# Per ADR-006, each plugin can accept external collector input at
# /api/collector/<pluginname>. The framework checks
# COLLECTOR_API_KEY_<PLUGINNAME> first, then COLLECTOR_API_KEY as fallback.
# COLLECTOR_API_KEY=
# COLLECTOR_API_KEY_COMPUTERS=
# COLLECTOR_API_KEY_ENDPOINTS=
# ---- Trusted plugin publisher keys (ADR-013, optional) ----
# Public-key PEM paths (OS path separator: ':' on Linux, ';' on Windows) used
# to verify signed plugin artifacts. Delivered with this config, NEVER from the
# plugin shelf. Empty on a site that does not adopt marketplace plugins.
# PLUGIN_TRUSTED_KEYS=/etc/shopdb/keys/curator.pub
#
# Enforce signatures: a plugin only loads/migrates if its tree matches a
# trusted signature. Default off. Turn on only after stamping plugins
# (flask plugin stamp-bundled) and pinning keys above.
# PLUGIN_REQUIRE_SIGNED=false
#
# Dev-only: directories whose UNSIGNED plugins are trusted, honored ONLY under
# DEBUG/TESTING (external-repo/symlink dev). Production ignores this.
# PLUGIN_DEV_TRUST_DIRS=/home/dev/my-plugin-repo
#
# Read-only folder the app pulls plugin artifacts from (a SharePoint-synced or
# copied shelf). The app reads this folder; it never speaks any network.
# PLUGIN_SHELF_DIR=/srv/shopdb/plugin-shelf
# ---- Employee directory database (optional, read-only) ----
# Separate HR/employee lookup DB consumed by the notifications plugin and the
# public shopfloor kiosks. Leave unset if the feature is not used; there is no
# safe default for the password, so an unset password fails loud.
# EMPLOYEE_DB_HOST=
# EMPLOYEE_DB_USER=
# EMPLOYEE_DB_PASSWORD=
# EMPLOYEE_DB_NAME=wjf_employees
# ---- cmmc_usb database (USB check-in/out) ----
# Separate read-write MySQL DB used by the USB plugin to track device
# check-in/out, lockers, and the check-in/out log. Leave unset if the feature
# is not used; there is no safe default for the password, so an unset password
# fails loud.
# CMMC_USB_DB_HOST=
# CMMC_USB_DB_USER=
# CMMC_USB_DB_PASSWORD=
# CMMC_USB_DB_NAME=cmmc_usb
# ---- Subpath deployment (optional) ----
# Serve the app under a URL prefix instead of the server root, e.g. as an IIS
# Application at /ops under an existing site. The frontend must be rebuilt with
# the matching base: VITE_BASE_PATH=/ops/ npm run build. Leave unset when the
# app owns its own site/port (the default). See the INSTALL-WINDOWS-IIS wiki page.
# MOUNT_PATH=/ops