diff --git a/IIS.md b/IIS.md index 4734dd9..0824cbb 100644 --- a/IIS.md +++ b/IIS.md @@ -12,9 +12,10 @@ - Set environment variables in web.config or IIS config: - `ASPNETCORE_ENVIRONMENT=Production` - `ADMIN_PASSWORD=` -- `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. - 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 `` in `wwwroot/index.html` for production so API calls include the subpath (keep blank for local/root). ## Permissions - Grant modify rights to the app pool identity on `App_Data` (DB file + wal). diff --git a/TASKS.md b/TASKS.md index d2a3fbc..00e436e 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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 - 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 - 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 `` or `` 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 `` or `` 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 - 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. diff --git a/wwwroot/index.html b/wwwroot/index.html index ab1c8e4..fb9f118 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -10,7 +10,7 @@ - + diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 3a136a1..3b2a97a 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -1,13 +1,15 @@ const defaultHeaders = { "Content-Type": "application/json" }; -const metaBase = document.querySelector('meta[name="app-base"]')?.content || ""; -const autoBase = (() => { - const parts = window.location.pathname.split("/").filter(Boolean); - return parts.length ? `/${parts[0]}` : ""; -})(); -const basePath = metaBase || autoBase; +const rawBase = document.querySelector('meta[name="app-base"]')?.content || ""; +const basePath = normalizeBase(rawBase); 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 } = {}) { const res = await fetch(withBase(path), { method,