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.
158
Web-Dashboard.md
Normal file
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.
|
||||
Reference in New Issue
Block a user