Fix client base path handling via explicit meta

This commit is contained in:
2026-02-05 16:42:31 +01:00
parent a6265e8656
commit f381c945a7
4 changed files with 12 additions and 9 deletions

3
IIS.md
View File

@@ -12,9 +12,10 @@
- Set environment variables in web.config or IIS config: - Set environment variables in web.config or IIS config:
- `ASPNETCORE_ENVIRONMENT=Production` - `ASPNETCORE_ENVIRONMENT=Production`
- `ADMIN_PASSWORD=<your-secret>` - `ADMIN_PASSWORD=<your-secret>`
- `BasePath=/vote` (only if the site is under a subfolder; omit for root) - `BasePath=/picknplay` (only if the site is under a subfolder; omit for root)
- Optional: enable stdout logging in `web.config` during troubleshooting only; disable afterward. - Optional: enable stdout logging in `web.config` during troubleshooting only; disable afterward.
- Data protection keys are persisted to `App_Data/keys`; ensure this folder is deployed and writable so auth cookies stay valid across app pool recycles. - Data protection keys are persisted to `App_Data/keys`; ensure this folder is deployed and writable so auth cookies stay valid across app pool recycles.
- Frontend base path: set `<meta name="app-base" content="/picknplay">` in `wwwroot/index.html` for production so API calls include the subpath (keep blank for local/root).
## Permissions ## Permissions
- Grant modify rights to the app pool identity on `App_Data` (DB file + wal). - Grant modify rights to the app pool identity on `App_Data` (DB file + wal).

View File

@@ -5,6 +5,6 @@
- [ ] **High - SSRF surface in screenshot validation** (`Endpoints/EndpointHelpers.IsReachableImageAsync`, `SuggestEndpoints.cs`): The server performs HEAD/GET requests to arbitrary user-provided URLs with minimal limits, enabling internal port probing and latency spikes. Restrict to allowlisted hosts or disable remote fetch; at minimum block private IPs/localhost, disallow redirects, and enforce tight size/time limits. - [ ] **High - SSRF surface in screenshot validation** (`Endpoints/EndpointHelpers.IsReachableImageAsync`, `SuggestEndpoints.cs`): The server performs HEAD/GET requests to arbitrary user-provided URLs with minimal limits, enabling internal port probing and latency spikes. Restrict to allowlisted hosts or disable remote fetch; at minimum block private IPs/localhost, disallow redirects, and enforce tight size/time limits.
- [ ] **High - Phase recalculation writes on every poll** (`Endpoints/EndpointHelpers.GetPhase`, `wwwroot/app.js`): `GetPhase` unconditionally calls `SaveChangesAsync`; the client polls `/api/state` every 4s, causing constant writes and SQLite WAL churn/locks even when nothing changes. Track a `changed` flag and persist only when state mutations occur; consider reducing poll frequency or using long polling/server push later. - [ ] **High - Phase recalculation writes on every poll** (`Endpoints/EndpointHelpers.GetPhase`, `wwwroot/app.js`): `GetPhase` unconditionally calls `SaveChangesAsync`; the client polls `/api/state` every 4s, causing constant writes and SQLite WAL churn/locks even when nothing changes. Track a `changed` flag and persist only when state mutations occur; consider reducing poll frequency or using long polling/server push later.
- [ ] **Medium - Admin key accepted via query string** (`Endpoints/EndpointHelpers.IsAdmin`): Accepting `key` in the query leaks secrets via logs and referrers. Require the `X-Admin-Key` header (or authenticated admin account) only, and rate-limit/monitor attempts. - [ ] **Medium - Admin key accepted via query string** (`Endpoints/EndpointHelpers.IsAdmin`): Accepting `key` in the query leaks secrets via logs and referrers. Require the `X-Admin-Key` header (or authenticated admin account) only, and rate-limit/monitor attempts.
- [ ] **Medium - Brittle base path detection on the client** (`wwwroot/js/api.js`): The heuristic that picks the first path segment can double-prefix or break when the app is nested (e.g., `/apps/vote/`). Prefer explicit `<meta name="app-base">` or `<base>` and drop the autodetect fallback. - [x] **Medium - Brittle base path detection on the client** (`wwwroot/js/api.js`): The heuristic that picks the first path segment can double-prefix or break when the app is nested (e.g., `/picknplay/`). Prefer explicit `<meta name="app-base">` or `<base>` and drop the autodetect fallback.
- [ ] **Maintenance - Centralize auth/phase concerns** (multiple endpoints): Each endpoint manually fetches the player, admin status, and phase gating, repeating logic and increasing drift risk. Introduce endpoint filters/middleware to attach the current player and enforce phase/admin requirements declaratively. - [ ] **Maintenance - Centralize auth/phase concerns** (multiple endpoints): Each endpoint manually fetches the player, admin status, and phase gating, repeating logic and increasing drift risk. Introduce endpoint filters/middleware to attach the current player and enforce phase/admin requirements declaratively.
- [ ] **Maintenance - Clear invalid cookies** (`Infrastructure/PlayerIdentityExtensions.cs`): When a cookie references a deleted player, we keep reissuing it; endpoints respond 401 but the cookie persists. Clear the cookie if lookup fails to avoid client loops. - [ ] **Maintenance - Clear invalid cookies** (`Infrastructure/PlayerIdentityExtensions.cs`): When a cookie references a deleted player, we keep reissuing it; endpoints respond 401 but the cookie persists. Clear the cookie if lookup fails to avoid client loops.

View File

@@ -10,7 +10,7 @@
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap" rel="stylesheet">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<!-- Optional: set data-app-base if served from a subfolder (e.g., /vote) --> <!-- Set to "/picknplay" in production; leave blank for localhost/root -->
<meta name="app-base" content=""> <meta name="app-base" content="">
</head> </head>
<body class="page"> <body class="page">

View File

@@ -1,13 +1,15 @@
const defaultHeaders = { "Content-Type": "application/json" }; const defaultHeaders = { "Content-Type": "application/json" };
const metaBase = document.querySelector('meta[name="app-base"]')?.content || ""; const rawBase = document.querySelector('meta[name="app-base"]')?.content || "";
const autoBase = (() => { const basePath = normalizeBase(rawBase);
const parts = window.location.pathname.split("/").filter(Boolean);
return parts.length ? `/${parts[0]}` : "";
})();
const basePath = metaBase || autoBase;
const withBase = (path) => `${basePath}${path}`; const withBase = (path) => `${basePath}${path}`;
function normalizeBase(value) {
if (!value) return "";
if (!value.startsWith("/")) return `/${value}`;
return value.endsWith("/") ? value.slice(0, -1) : value;
}
async function request(path, { method = "GET", body } = {}) { async function request(path, { method = "GET", body } = {}) {
const res = await fetch(withBase(path), { const res = await fetch(withBase(path), {
method, method,