diff --git a/DorkNet.AdminMobile/README.md b/DorkNet.AdminMobile/README.md index dabe05a..524f4a1 100644 --- a/DorkNet.AdminMobile/README.md +++ b/DorkNet.AdminMobile/README.md @@ -4,8 +4,13 @@ Native Android/iOS admin app for DorkNet, built with .NET MAUI. In the microservices deployment, point the app at the public admin host for the stack, usually `https://admin.`. Cloudflare and -`DorkNet.Gateway` keep the public URL stable while admin API requests are -routed to the owning backend slice. +`DorkNet.Gateway` keep the public URL stable. Requests made through the +admin host are handled by the `web` service's same-origin admin route; +the `/api/admin/*` path family is also exposed by the moderation slice +for non-admin-host routing. + +The browser admin page, route map, and troubleshooting flow are +documented in [`../docs/admin.md`](../docs/admin.md). ## Authentication diff --git a/DorkNet.Server/Data/MIGRATIONS.md b/DorkNet.Server/Data/MIGRATIONS.md index ebc7fd8..34d0104 100644 --- a/DorkNet.Server/Data/MIGRATIONS.md +++ b/DorkNet.Server/Data/MIGRATIONS.md @@ -30,7 +30,7 @@ Both were collapsed in 2026-05-25: no per-feature patches needed. - The PhotonRegion coerce (the only data transform in the block) was moved into `LegacyUpgrades.CoercePhotonRegionAsync`. -- The Storage admin page (`admin-ui/src/pages/Storage.tsx`) and +- The legacy storage backfill page (`admin-ui/src/pages/Storage.tsx`) and `StorageBackfillController.cs` were deleted — they were the UI for draining the legacy `RoomDataBlobs.Bytes` column into S3, which is no longer needed. diff --git a/DorkNet.Server/Data/url-coverage.md b/DorkNet.Server/Data/url-coverage.md index 07bb0e6..cc4e4f9 100644 --- a/DorkNet.Server/Data/url-coverage.md +++ b/DorkNet.Server/Data/url-coverage.md @@ -2,6 +2,13 @@ Generated: 2026-05-09. Source: client URLs from `Cpp2IL_ISIL` ISIL dump (curated list provided to this report); server routes harvested via grep of `[HttpGet]`/`[HttpPost]`/`[HttpPut]`/`[HttpDelete]`/`[Route]` attributes across `Controllers/**/*.cs` plus the namespace-wildcard fall-throughs in `ApiNamespaceStubsController` and the global `ApiCatchAllController` / `GlobalCatchAllController`. +> Snapshot scope: this file is a historical controller-coverage matrix, +> not the live gateway route table. The dedicated microservice hosts +> still reuse the shared controller stack behind route guards, so the +> endpoint shape notes remain useful. For current service ownership, use +> `DorkNet.Contracts/DorkNetRouteOwnership.cs` or the gateway +> `/internal/routes` endpoint. + ## How this matrix is built Each client URL is matched against the server's full route table. ASP.NET Core routes resolve "specific wins over wildcard", so a route literally registered as `api/foo/v1/bar` always wins over `api/foo/{*path}`. The `Status` column reflects the route that actually wins: diff --git a/README.md b/README.md index 121d1d5..9bfd528 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,9 @@ For deployment details, including Dokploy env vars, Cloudflare Tunnel routing, external Postgres, and S3 setup, see [docs/deploy.md](docs/deploy.md). +For the browser admin console, its routes, auth model, and production +troubleshooting, see [docs/admin.md](docs/admin.md). + --- ## Database & migrations diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 0000000..9200f8e --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,136 @@ +# DorkNet Admin Page + +The admin page is the browser operations console for DorkNet. In the +microservices deployment it lives at `https://admin./` and is +served by the `web` service from the built Vite SPA assets. + +The admin backend endpoints are under `/api/admin/v1`. Requests from the +SPA are same-origin, so `https://admin./api/admin/v1/*` is also +handled by the `web` service route on the admin host. The same +`/api/admin/*` route family is available as a path-owned moderation slice +for non-admin-host routing, so `/internal/routes` can show moderation as +the owner for that path family. + +## Runtime Routing + +| Item | Value | +|---|---| +| SPA source | `DorkNet.Server/admin-ui` | +| Build output | `DorkNet.Server/wwwroot/admin` | +| Container path | `/app/wwwroot/admin` | +| Public browser host | `admin.` | +| API base used by the SPA | `/api/admin/v1` | +| Login endpoint | `POST /api/admin/v1/login` | +| Same-origin admin-host service | `web` | +| Path-routed admin API slice | `moderation` | + +`DorkNet.Gateway` preserves the original Host header when it proxies to +backend services. That matters for the admin page: the `admin.` +host route must reach `web`, and `web` must contain +`/app/wwwroot/admin/index.html`. + +## Authentication + +The first account created on a fresh database is promoted to admin so +there is always a bootstrap operator. The login form posts to +`/api/admin/v1/login`; the SPA stores the returned values in browser +`localStorage`: + +| Key | Contents | +|---|---| +| `dorknet.admin.token` | DorkNet admin JWT | +| `dorknet.admin.me` | Admin identity object used by the layout | + +Every admin API request sends `Authorization: Bearer `. Protected +actions are guarded by `AdminOnlyAttribute`, which checks the resolved +player row still has `IsAdmin = true`. A `401` clears the local session +and sends the browser back to login. + +If the admin host is public, put it behind Cloudflare Access or an +equivalent outer access control. The DorkNet JWT is still required after +that outer check. + +## Navigation + +| Section | Routes | Workflows | +|---|---|---| +| Overview | `/` | Live ops dashboard, online players, active sessions, quick kick/ban/broadcast actions | +| Moderation | `/players` | Player directory, bans, reports, per-player ban/grant/gift/password/avatar/account actions | +| Activity | `/activity` | Admin audit log and per-player request logs | +| Content | `/rooms`, `/rooms/:id`, `/import-room`, `/content` | Room list/detail, room import, instances, leaderboards, community board, loading tips | +| Operations | `/broadcast`, `/settings` | Server broadcast, server toggles, signup codes, weekly challenges, Play menu tags, Rec Center doors, game config values | + +Several older admin URLs are kept as redirects: + +| Old route | Current route | +|---|---| +| `/bans` | `/players?tab=bans` | +| `/reports` | `/players?tab=reports` | +| `/gift`, `/passwords`, `/grants` | `/players` | +| `/audit` | `/activity?tab=audit` | +| `/logs` | `/activity?tab=logs` | +| `/rr-originals`, `/instances`, `/leaderboards` | `/rooms` | +| `/community` | `/content?tab=community` | +| `/loading-tips` | `/content?tab=tips` | +| `/signup-codes` | `/settings?tab=signup` | + +`/import-room-legacy` still exists for the legacy room importer. + +## Build And Deploy + +The service Dockerfile builds the admin SPA for service images that need +static assets. In the microservices stack the `web` image must include: + +```text +/app/wwwroot/admin/index.html +/app/wwwroot/admin/assets/* +``` + +The admin SPA calls relative URLs, so it does not need a configured API +origin as long as the browser is opened on `https://admin./`. + +The native admin mobile app points at the same public admin host; see +[`../DorkNet.AdminMobile/README.md`](../DorkNet.AdminMobile/README.md). + +## Smoke Checks + +Check the static host: + +```bash +curl -I https://admin.yourdomain.com/ +``` + +Check that protected admin API routing is alive. A `401` is expected +without a valid admin token: + +```bash +curl -i https://admin.yourdomain.com/api/admin/v1/stats +``` + +Check the built assets inside the running `web` container: + +```bash +docker exec sh -lc 'find /app/wwwroot/admin -maxdepth 2 -type f | sort | head -50' +``` + +Useful logs: + +```bash +docker logs -gateway-1 --tail=200 +docker logs -web-1 --tail=200 +docker logs -moderation-1 --tail=200 +``` + +For normal browser admin traffic, start with `gateway` and `web`. +Check `moderation` when you are testing `/api/admin/*` through a +non-admin host such as `api.`. + +## Troubleshooting + +| Symptom | Check | +|---|---| +| `admin./` returns 404 | `web` logs for `probe="/app/wwwroot/admin/index.html" exists=False`; rebuild/redeploy the `web` image | +| Admin page loads but actions 404 | Gateway `/internal/routes`, then whether the request host is `admin.` or a path-routed host | +| Login or actions return 401 | Token expired, local session cleared, or the player no longer has `IsAdmin = true` | +| Browser spins forever | Open the browser network tab, then check `gateway` and `web` logs for the stuck URL | +| Upload/import fails near 100 MB | Use the chunked importer; the SPA uses 50 MB chunks to stay below Cloudflare request limits | diff --git a/docs/architecture.md b/docs/architecture.md index d254f5d..b55d9e2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -45,8 +45,8 @@ DorkNet.sln | `DorkNet.Services.Social` | Clubs, groups, announcements, player events, subscriptions | .NET 9 | | `DorkNet.Services.Commerce` | Catalog, storefronts, econ, inventory, inventions | .NET 9 | | `DorkNet.Services.Platform` | Service URLs, config, version checks, geo, strings, telemetry-style surfaces | .NET 9 | -| `DorkNet.Services.Moderation` | Bug reporting, player reporting, sanitize, admin API, testcase routes | .NET 9 | -| `DorkNet.Services.Web` | Public site, admin static host, feed static host, site API | .NET 9 | +| `DorkNet.Services.Moderation` | Bug reporting, player reporting, sanitize, path-routed admin API, testcase routes | .NET 9 | +| `DorkNet.Services.Web` | Public site, admin static host, same-origin admin browser API, feed static host, site API | .NET 9 | | `DorkNet.ClientMod` | MelonLoader 0.6.x IL2CPP mod — the client patcher, JSON config | .NET 6 | `ClientMod` applies the client-side patches needed to point the 2020 @@ -119,6 +119,10 @@ SPAs for service images that need static assets, publishes the selected .NET project, and runs that DLL in the runtime image. The root `Dockerfile` builds only the standalone/fallback `DorkNet.Server` image. +For the admin browser console specifically, including the host routing +split between the admin static host and `/api/admin/v1`, see +[`admin.md`](admin.md). + ### Controllers/ groupings 20 top-level groups, each responsible for one product surface the diff --git a/docs/deploy.md b/docs/deploy.md index 51a44fb..c2c14ad 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -29,8 +29,8 @@ while route slices move out of the fallback server. | `social` | Clubs, groups, announcements, player events, subscriptions | | `commerce` | Catalog, storefronts, econ, inventory, inventions | | `platform` | Service directory, config, version checks, geo, strings | -| `moderation` | Bug reports, player reports, sanitize, admin API, testcase routes | -| `web` | Apex/www/admin/feed static hosts and site API | +| `moderation` | Bug reports, player reports, sanitize, path-routed admin API, testcase routes | +| `web` | Apex/www/admin/feed static hosts, same-origin admin browser API, and site API | | `monolith` | Fallback shared server for route families not split yet | | `postgres` | Bundled Postgres for the compose network | | `redis` | Bundled Redis for ephemeral state and fan-out | @@ -240,11 +240,15 @@ Expected key files: slice or to the monolith fallback. `/internal/services/health` reports gateway-visible health for each backend service. +For the admin page's browser routes, auth flow, same-origin API calls, +and focused troubleshooting, see [`admin.md`](admin.md). + ### Useful Logs ```bash docker logs -gateway-1 --tail=200 docker logs -web-1 --tail=200 +docker logs -moderation-1 --tail=200 docker logs -platform-1 --tail=200 docker logs -cloudflared-1 --tail=200 ``` @@ -260,6 +264,10 @@ That means the `web` image did not include the built SPA assets or the old container is still running. Force a clean Dokploy rebuild on the latest branch commit. +Admin browser traffic on `admin.` is routed to `web`. The +`moderation` service is still useful when testing the `/api/admin/*` +path family through non-admin hosts. + --- ## Local Microservices diff --git a/tools/cloudflared.README.md b/tools/cloudflared.README.md index 48deb83..a57ce5e 100644 --- a/tools/cloudflared.README.md +++ b/tools/cloudflared.README.md @@ -3,7 +3,9 @@ Cloudflare Tunnel is used in two DorkNet workflows: - Local standalone testing, where cloudflared forwards `*.localhost` - traffic to a locally running `DorkNet.Server`. + traffic to a locally running `DorkNet.Server`. The default origin is + `http://localhost:8080`; use local HTTPS only when you intentionally + run the standalone server with a wildcard dev certificate. - Dokploy production microservices, where the compose sidecar forwards the apex and wildcard hostnames to `http://gateway:8080`. @@ -26,10 +28,9 @@ friends to reach it through Cloudflare. 1. **`localhost` is on a Cloudflare account you control.** The DNS nameservers for the domain need to be Cloudflare's. Check at — the zone should be Active. -2. **Local standalone server running** on `0.0.0.0:443` with the wildcard cert - that covers `*.localhost` (re-run `tools/patch-client.ps1` if your - cert is still the `+1` rec.net-only one — it'll regenerate as - `+3` covering both domains). +2. **Local standalone server running.** The current quickstart listens + on `http://localhost:8080`. Older local TLS setups can still run on + `0.0.0.0:443` with a wildcard certificate that covers `*.localhost`. 3. **Patched client** with `tools/patch-domain.ps1` so the in-game URLs resolve to `*.localhost`. (Players using the public deployment need to run this on their own client too.) @@ -97,7 +98,7 @@ visitor -> https://api.localhost/api/versioncheck/v4 -> Cloudflare edge -> cloudflared tunnel -> cloudflared running on your machine - -> https://localhost:443 (Host preserved) + -> http://localhost:8080 (Host preserved) -> DorkNet.Server ``` @@ -112,12 +113,14 @@ visitor -> https://api.yourdomain.com/api/versioncheck/v4 -> dedicated service slice or monolith fallback ``` -For the local standalone template, the leg from cloudflared to localhost -has `noTLSVerify: true` because the local server's cert is from mkcert -(private CA only trusted on your machine). The Cloudflare-edge to -cloudflared leg is still TLS-encrypted with Cloudflare's cert. In the -Dokploy microservices stack, the tunnel sidecar uses plain HTTP on the -private Compose network and sends everything to the gateway. +For default local testing, point the local cloudflared origin at +`http://localhost:8080`. If you intentionally test a local HTTPS origin, +the leg from cloudflared to localhost can use `noTLSVerify: true` +because that certificate is usually from a private dev CA. The +Cloudflare-edge to cloudflared leg is still TLS-encrypted with +Cloudflare's cert. In the Dokploy microservices stack, the tunnel +sidecar uses plain HTTP on the private Compose network and sends +everything to the gateway. ## Tearing down