Add secure public driver training profiles
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user