Add secure public driver training profiles

This commit is contained in:
larssand
2026-08-24 16:54:35 +02:00
parent c2ed00861a
commit 4a74785791
12 changed files with 1360 additions and 12 deletions

View File

@@ -126,6 +126,7 @@ Owns:
- file watching for app version
- AMMC config + process state
- public overlay route serving via same frontend shell
- tokenized public driver training profile API and standalone mobile shell
- PDF export endpoint
Main API groups:
@@ -138,11 +139,20 @@ Main API groups:
- `/api/ammc/start`
- `/api/ammc/stop`
- `/api/export/pdf`
- `/public-training/api/:shareToken`
Static routes of interest:
- `/`
- `/public-overlay`
- `/public-overlay/obs`
- `/public-training/:shareToken`
Runtime environment variables:
- `PORT`: HTTP port, default `8081`
- `HOST`: bind address, default `0.0.0.0`
- `RC_TIMING_DATA_DIR`: optional absolute/relative data directory override; defaults to `<repo>/data`
- `RC_TIMING_PID_FILE`: optional PID-file path used by process control
- `PUBLIC_OVERLAY_TOKEN`: optional shared token for public overlay routes
### `scripts/serverctl.js`
Role:
@@ -428,12 +438,48 @@ Owns:
Mutations:
- replaces or appends entries in `state.training.goals`
- creates, rotates, enables, or disables entries in `state.training.shares`
- always calls injected `saveState()` before rerender
Route behavior:
- `/?view=training` opens the training view directly
- `driver=<driverId>` selects a driver and supports mobile bookmarks
- this is currently the same trusted-LAN admin shell, not a separate public read-only profile route
- an explicitly enabled driver receives `/public-training/<shareToken>` as a separate read-only profile
- replacing or disabling the share invalidates its old route immediately after state sync
### `public-training.html`
Type:
- standalone public-profile entry document
Loads:
- `/src/public_training.css`
- `/src/public_training.js`
Served only after `server.js` validates the enabled share token in `/public-training/:shareToken`.
### `src/public_training.js`
Type:
- standalone public profile frontend
Owns:
- fetching only `/public-training/api/:shareToken`
- rendering one driver's sanitized statistics, goals, rewards, and daily history
- local 20-minute practice timer with 10-minute push/control phases
- Swedish/English copy selected by `lang` query or browser locale
Important isolation:
- does not import `app.js`
- does not call `/api/state`
- does not mutate backend or race state
- timer state is stored in the mobile browser under a token-scoped localStorage key
### `src/public_training.css`
Type:
- standalone responsive public-profile stylesheet
Owns:
- phone-first layout independent from the admin sidebar/theme system
- profile, level, timer, goals, and daily-history presentation
## 3. Direct Import Dependencies
@@ -832,6 +878,9 @@ sequenceDiagram
| `src/settings.js` | view/render | none | `app.js` | yes | no | no |
| `src/training_logic.js` | logic | none | `app.js` | no | no | no, returns normalized/derived data |
| `src/training_views.js` | view/controller | none | `app.js` | yes | no | yes, through injected state access |
| `public-training.html` | standalone entry | `public_training.css`, `public_training.js` | `server.js` route | root only | no | no |
| `src/public_training.js` | standalone public view | none | `public-training.html` | yes | local timer only | no backend mutation |
| `src/public_training.css` | standalone stylesheet | n/a | `public-training.html` | n/a | no | no |
| `server.js` | backend runtime | node core + deps | npm/node | n/a | backend process state | yes |
| `scripts/serverctl.js` | process control | node core | npm scripts | n/a | no | pid/log files only |
@@ -1442,7 +1491,8 @@ This bucket is the most dynamic part of state and is mutated heavily during live
```js
{
goals: TrainingGoal[],
ledger: TrainingLap[]
ledger: TrainingLap[],
shares: TrainingShare[]
}
```
@@ -1460,6 +1510,24 @@ This bucket is the most dynamic part of state and is mutated heavily during live
}
```
`TrainingShare`:
```js
{
driverId: string,
token: string, // 24 random bytes encoded as base64url (192 bits)
enabled: boolean,
createdAt: string
}
```
Share rules:
- no share exists until an admin explicitly enables the selected driver profile
- only one normalized share entry is retained per driver
- disabled shares fail public route lookup
- rotating a share replaces its token; the previous URL no longer resolves after persistence
- tokens are generated in the browser with `crypto.getRandomValues()`, never from the shorter UI `uid()` helper
`TrainingLap`:
```js
@@ -1814,13 +1882,45 @@ Routes:
- `/public-overlay/:mode`
Behavior:
- if `PUBLIC_OVERLAY_TOKEN` is set, query param `token` must match
- otherwise returns `403 Forbidden`
- if `PUBLIC_OVERLAY_TOKEN` is set, query param `token` must match or the route returns `403 Forbidden`
- if no token environment variable is set, the overlay route is available without a query token
- on success serves `index.html`
Use for:
- public OBS/browser-source overlays without exposing admin path directly
## 16.14 Public training profile routes
Routes:
- page: `GET /public-training/:shareToken`
- data: `GET /public-training/api/:shareToken`
Authorization:
- `shareToken` must match an enabled entry in `state.training.shares`
- token length is at least 24 characters after normalization
- missing, disabled, replaced, or unknown tokens return `404` rather than revealing whether a driver exists
Sanitized response fields:
- selected driver display name, class, and brand
- numeric club rank without other driver names
- aggregate totals, best/average time, consistency, controlled streak, and daily summaries
- next automatic milestone
- recommendation level and target lap
- selected driver's goal titles, reward text, progress, and claimed state
- public club branding and safe PNG/JPEG/WebP logo data
Explicitly excluded:
- transponder IDs
- driver IDs and names of other drivers
- cars, events, sessions, assignments, and raw passings
- race settings and decoder/AMMC configuration
- complete `/api/state` payload
Static serving boundary:
- Express serves static files only under `/src` and `/docs`
- `data/`, `AMMC/`, `server.js`, scripts, manifests, and package files are not static downloads
- `public-training.html` and `index.html` are served only by explicit routes/fallback handlers
## 17. Regression History / Known Gotchas
This section captures bugs already seen in this codebase after the modular split.