Add Web-Dashboard page (Unit 12)

Covers decnet web / serve-web, env vars, admin credential enforcement,
pages (Dashboard, DeckyFleet, LiveLogs, Attackers, AttackerDetail,
Config, Bounty), JWT auth flow, must_change_password, server-side UI
gating, client build, and CORS defaults.
2026-04-18 06:05:22 -04:00
parent 9de6ea3dff
commit 82080995d7

158
Web-Dashboard.md Normal file

@@ -0,0 +1,158 @@
# Web Dashboard
The DECNET Web Dashboard is a React + TypeScript + Vite single-page app that
talks to the FastAPI backend exposed by the DECNET API server. It is the
operator-facing control surface for inspecting deckies, attackers, live logs,
bounties, and runtime configuration.
Related:
[REST API](REST-API-Reference) -
[Environment variables](Environment-Variables) -
[Security & stealth](Security-and-Stealth).
## Running
Two processes are involved: the API server (FastAPI) and the web front-end
(static SPA with a thin `/api/*` reverse proxy).
```bash
# Start the FastAPI backend (serves the REST API on DECNET_API_PORT).
decnet serve-api
# Serve the Web Dashboard (static SPA).
decnet web \
--host 0.0.0.0 \
--web-port 8080 \
--api-port 8000
```
`decnet web` loads the pre-built bundle from `decnet_web/dist/` and refuses to
start if that directory is missing. The handler proxies any request whose path
starts with `/api/` to the backend on `--api-port`, so the SPA can use relative
URLs and no CORS round-trip is needed in the default topology. Add `--daemon`
to detach into the background.
### Environment variables
| Variable | Default | Purpose |
|---|---|---|
| `DECNET_WEB_HOST` | `127.0.0.1` | Bind address for the SPA server. |
| `DECNET_WEB_PORT` | `8080` | Port for the SPA server. |
| `DECNET_ADMIN_USER` | `admin` | Admin username seeded on first boot. |
| `DECNET_ADMIN_PASSWORD` | `admin` | Admin password seeded on first boot. |
| `DECNET_JWT_SECRET` | required | HS256 signing secret, must be at least 32 characters. |
| `DECNET_CORS_ORIGINS` | `http://<web_host>:<web_port>` | Comma-separated allowlist for backend CORS. |
The CORS default resolves wildcard bind addresses (`0.0.0.0`, `::`) to
`localhost` when building the default origin. Override with something like:
```
DECNET_CORS_ORIGINS=http://192.168.1.50:9090,https://dashboard.example.com
```
### Admin credentials
The literal pair `admin` / `admin` is a known-bad default. The web layer
refuses to start with those values outside pytest: set **both**
`DECNET_ADMIN_USER` and `DECNET_ADMIN_PASSWORD` to non-default values before
running `decnet web` or `decnet serve-api` in production. See the admin
credentials section in [Environment variables](Environment-Variables) for the
exact enforcement rules.
On first boot the admin record is seeded with `must_change_password=True`, and
the frontend forces a password change on the next login.
## Pages
The routes are defined in `decnet_web/src/App.tsx`. All pages share the
`Layout` chrome (top bar with search + logout, left nav).
- **Dashboard** (`/`, `Dashboard.tsx`) - Landing view. Summary tiles for decky
count, attacker count, recent log volume, and top-level activity charts. The
top-bar search box is threaded in as `searchQuery` so the dashboard can
filter its recent-events pane inline.
- **DeckyFleet** (`/fleet`, `DeckyFleet.tsx`) - Fleet control panel. Lists
every decky with status, IP, services, and mutation interval. Admin-only
controls let you redeploy from an INI payload, mutate or kill individual
deckies, and retune mutation intervals. Backed by the `fleet/` router group.
- **LiveLogs** (`/live-logs`, `LiveLogs.tsx`) - Streaming log tail across the
whole fleet. Events are parsed with `utils/parseEventBody.ts` to pull out
attacker IP, decky, service, and RFC 5424 structured data fields. An
artifact drawer (`ArtifactDrawer.tsx`) pops open when a log line references
an uploaded payload.
- **Attackers** (`/attackers`, `Attackers.tsx`) - Grouped view of observed
source IPs with hit counts, first/last seen, and targeted services. Click
through to the detail page.
- **AttackerDetail** (`/attackers/:id`, `AttackerDetail.tsx`) - Full
per-attacker timeline: every log line tied to that IP, captured credentials
and commands, any uploaded artifacts, and bounty status if applicable.
- **Config** (`/config`, `Config.tsx`) - Runtime configuration and user
management. Admins can view and edit the running INI config, reinitialize
the fleet, and create, update, or delete dashboard users. Non-admins cannot
reach this page; see "Server-side UI gating" below.
- **Bounty** (`/bounty`, `Bounty.tsx`) - Displays bounty offers and redemption
state attached to particular attacker behaviors. Read-only view over the
`bounty/` router group.
## Authentication
The backend exposes `POST /auth/login` (see
`decnet/web/router/auth/api_login.py`). The handler loads the user via a
cached lookup, verifies the password with bcrypt off the event loop, and
returns a `Token` payload:
```json
{
"access_token": "<JWT>",
"token_type": "bearer",
"must_change_password": false
}
```
JWT details (`decnet/web/auth.py`):
- Algorithm: **HS256**.
- Secret: `DECNET_JWT_SECRET` (must be at least 32 characters; short or
known-bad values abort startup).
- Lifetime: **1440 minutes** (24 hours).
- Claims: `uuid` (user UUID), `iat`, `exp`.
The SPA stores the token in `localStorage` and decodes the `exp` claim on
boot to decide whether to show the login screen (`App.tsx`).
### must_change_password flow
When the login response carries `must_change_password: true` the SPA forces a
password-change dialog before any other page loads. Internally, the FastAPI
dependency `get_current_active_user` returns HTTP 403 for any protected route
while the flag is set, so the rest of the API is effectively gated until the
user calls `POST /auth/change-password`, which clears the flag via
`repo.update_user_password(..., must_change_password=False)`.
## Server-side UI gating
**Admin-only UI must be gated by the backend, not by client-side role
checks.** Every mutating or privileged endpoint under the `fleet/`, `config/`,
and `artifacts/` router groups is protected by the `require_admin` FastAPI
dependency (`decnet/web/dependencies.py`). The SPA may hide or disable admin
controls for UX, but that is a presentation convenience only - a non-admin
user who hand-crafts requests still gets HTTP 403 from the server. Do not
introduce features whose only access check is `if user.role === 'admin'` in
React; that check must exist on the server side as well.
## Client build
The front-end lives in `decnet_web/` and is a standard Vite project.
```bash
cd decnet_web
npm install
npm run build # production bundle -> decnet_web/dist/
npm run dev # hot-reload dev server (proxies /api/* to the backend)
```
`decnet web` expects the production bundle at `decnet_web/dist/` relative to
the installed package (`Path(__file__).parent.parent / "decnet_web" / "dist"`
in `decnet/cli.py`). If you are working on the SPA, rebuild before running
`decnet web`, or use `npm run dev` and point your browser at the Vite dev
server during development.