diff --git a/.env.example b/.env.example index 68e9ebbe..891adb9a 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,18 @@ DATABASE_URL=mysql+pymysql://shopdb:CHANGE_ME@db:3306/shopdb_flask?charset=utf8m # 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 diff --git a/docs/api-inventory.json b/docs/api-inventory.json index f5b94efb..e162133c 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -3757,11 +3757,11 @@ "endpoints": [ { "method": "GET", - "path": "/api/endpoints/", + "path": "/api/endpoints", "auth": "endpoints.view", "params": "query: search (hostname substring)", "purpose": "List endpoints reporting through the ShopDB agent, with last-seen and online state", - "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/" + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints" }, { "method": "GET", diff --git a/docs/openapi.json b/docs/openapi.json index 3fc3ff12..42be3046 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -21733,13 +21733,13 @@ ] } }, - "/api/endpoints/": { + "/api/endpoints": { "get": { "tags": [ "plugin-endpoints" ], "summary": "List endpoints reporting through the ShopDB agent, with last-seen and online state", - "description": "List endpoints reporting through the ShopDB agent, with last-seen and online state\n\n**Auth:** endpoints.view\n\n**Params:** query: search (hostname substring)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/\n```", + "description": "List endpoints reporting through the ShopDB agent, with last-seen and online state\n\n**Auth:** endpoints.view\n\n**Params:** query: search (hostname substring)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints\n```", "security": [ { "bearerAuth": [] diff --git a/plugins/endpoints/api/routes.py b/plugins/endpoints/api/routes.py index 07355173..4dade2c8 100644 --- a/plugins/endpoints/api/routes.py +++ b/plugins/endpoints/api/routes.py @@ -55,11 +55,25 @@ def _format_uptime(seconds): # Endpoint state # ============================================================================= -@endpoints_bp.route('/', methods=['GET']) +@endpoints_bp.route('', methods=['GET']) @require_permission('endpoints.view') def list_endpoints(): """List reported endpoint states. + Registered as '' so the rule is '/api/endpoints', with no trailing slash. + CONTRIBUTING.md's API section spells endpoints that way ('/api/machines'), + and every other plugin's list route follows it - this was the only route in + 331 paths that did not. + + It was not cosmetic. A rule ending in a slash makes Flask answer the + unslashed URL with a 308 to an ABSOLUTE url. On dev that resolves and + nobody notices. Behind the IIS subpath it does not: 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. The browser refuses + to follow an https page to http, the list request failed, and the page + rendered "No endpoints found" - indistinguishable from a fleet with nothing + in it. The agent had been reporting correctly the whole time. + Joins through Computer so each row carries computerid and can deep-link into the existing PC detail page (/pcs/{computerid}). Endpoint state only exists for hostnames resolved to a Computer, so the join is not lossy. diff --git a/plugins/endpoints/frontend/views/EndpointsList.vue b/plugins/endpoints/frontend/views/EndpointsList.vue index 802ccbef..313cbd72 100644 --- a/plugins/endpoints/frontend/views/EndpointsList.vue +++ b/plugins/endpoints/frontend/views/EndpointsList.vue @@ -18,6 +18,10 @@
Loading...
+
+ {{ loadError }} +
+