Login form is now email+password (was a bare shared password field). Nav conditionally
shows Instructors (owner only) and Audit (owner/admin) tabs based on the signed-in
instructor's role. New InstructorsView (create/list/edit accounts, role/status controls,
session list + revoke-all, password reset — owner only), AuditView (read-only audit event
table), AccountView (change-password form for the signed-in instructor). CohortsView hides
the create-cohort form and curriculum-assignment button for the 'instructor' role — the
server already enforces this; this just avoids showing controls that would 403.
Non-HttpOnly rn_csrf_s/rn_csrf_i cookies, set on login, echoed back as X-CSRF-Token on every
state-changing request — defense in depth on top of (not instead of) SameSite=Lax. In-memory
sliding-window rate limiting on instructor login (8 attempts / 5 min per ip:email, no new
dependency).
Real bug found while wiring this up: the Gateway's own progress-relay call (triggered when a
student solves a scenario) is a server-to-server POST to /api/progress/record relaying the
browser's session — it needs to forward the CSRF token too, and needs to forward BOTH the
rn_csrf_s cookie and a matching X-CSRF-Token header (forwarding only the header still 403'd,
since the double-submit check needs both on the request Platform API actually receives).
Also tightens the Gateway's session-resolution cache from 5000ms to 2000ms, so a
revoked/expired session stops working at the Gateway within a tighter, tested bound.
Frontend (admin + student API clients) now reads the relevant rn_csrf_* cookie via
document.cookie and attaches it on every POST/PATCH/DELETE.
New /admin route (frontend/src/admin/), gated by a separate admin
session (scrypt + timingSafeEqual against ADMIN_PASSWORD, no plaintext
storage, no new dependency). Covers cohort CRUD + roster + curriculum
assignment, and student CRUD + cohort assignment + environment/progress/
attempt views, all under /api/admin/* on the Platform API.
main.jsx now does a three-way path split (lab / admin / platform) so the
existing, unmodified Lab UI, the student dashboard, and the new admin
portal all ship in one Vite bundle without a router dependency.
Replaces the Phase 2A pilot ?sid= query param with an HttpOnly,
SameSite=Lax session cookie. Gateway now derives student identity from
the session (via Platform API's /api/session/whoami) instead of trusting
a client-suppliable id, and checks ownership on every proxied HTTP and
WebSocket Lab request, not just the initial navigation.
Fixes a routing bug found during testing: the fallback route was using
'does this session own a running environment' to decide whether an
unprefixed /api/* call belonged to the Lab UI or the dashboard — true in
both places. Replaced with an explicit LAB_OWNED_PATHS allowlist of the
exact unprefixed paths the existing Lab UI actually calls.
e2e-phase2a.js rewritten to use a cookie jar instead of headers/query
params, with explicit impersonation-rejection tests added. 10/10 passing.
StudentDashboard.jsx — "Up Next" curriculum view + Launch Lab flow, in one
component since they share state (environment status affects what curriculum
actions make sense).
- Renders GET /api/curriculum/path as-is: the existing RootNode path
(Foundation -> Workloads -> Networking -> Configuration -> Troubleshooting
-> Real Ops -> Capstone), no new structure invented. Capstone stays the
locked placeholder from the earlier slice.
- Launch Lab -> POST /api/environment/launch, polls status every 2.5s while
provisioning/resetting, transitions through none -> provisioning -> running
-> error with a distinct UI state for each (EnvironmentPanel). On running,
navigates to launch_path with the student's own id as ?sid= — a
defense-in-depth ownership check the Gateway verifies on that specific
navigation (see the gateway commit).
- One "Recreate Lab" control, calling POST /api/environment/reset — no raw
destroy-without-recreate exposed to students, matching "don't expose raw
destroy/admin controls beyond what is safe for a student." A visible note
states progress is saved separately and survives.
Platform.module.css reuses the existing design tokens from index.css
(--green, --surface, --text, etc.) so this reads as the same product as the
Lab UI, not a bolted-on separate app.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
New Platform Shell entry point (frontend/src/platform/) alongside the
existing, completely unmodified Lab UI (App.jsx and everything under
frontend/src/components/). main.jsx now splits on the current URL path —
/lab/<token>/ renders the existing <App/> exactly as before; anything else
renders the new <PlatformShell/>. Both live in the same built bundle (it's
served both by the persistent identity instance and by every ephemeral Lab
container), split purely at runtime.
- EntryScreen.jsx — cohort dropdown -> name dropdown (existing
GET /api/roster/cohorts, GET /api/roster?cohort=) -> POST /api/login. No
password. Visibly labeled pilot identity, not production auth, per the
governing requirement that this be stated explicitly, not just true in the
architecture docs.
- api.js — student_id lives in localStorage only, sent as X-Student-Id on
every authenticated request. Every authenticated call throws if no student
is stored rather than silently going out anonymously — this is what
"prevent accidental anonymous progress writes" means in practice, not just
a comment.
- PlatformShell.jsx — entry screen if no stored session, dashboard otherwise.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Pre-Phase-1 platform work, approved and tested earlier but not yet committed:
- Terminal banner: "RootNode Academy Lab — node: k8s-lab", upstream KubeKosh
attribution left untouched in footer/README/LICENSE.
- RootNode curriculum mode: scenarios/rootnode-path.json (Foundation -> Workloads
-> Networking -> Configuration -> Troubleshooting -> Real Ops -> Capstone),
built from existing scenario content grouped by category, no new scenarios
invented. New GET /api/curriculum/path endpoint.
- Student roster + per-student progress: instructor-roster, pick-your-name login
(no password), students/environments-adjacent progress table migrated to a
(student_id, scenario_id) composite key so practice progress and exam sessions
are isolated per student instead of one global bucket. New /api/roster,
/api/login endpoints.
- scenarios/data/*.json: added isolation_type metadata (shared_namespace vs
ephemeral_cluster) from an audit of which scenarios execute cluster-scoped or
node-level commands. This was Tier-1-isolation groundwork that got superseded
same-day by the full-ephemeral-per-student architecture (see
docs/LAB_ARCHITECTURE.md) — the field is kept as descriptive metadata only,
per the note now in scenarios/SCHEMA.md. Not wasted: it's an accurate audit of
which scenarios touch cluster-scoped resources, useful independent of isolation
model.
- .gitignore added (node_modules/, frontend/dist/, *.db, /data/, .env) — none
existed before; without it, npm installs during testing would have landed in
a commit.
Known limitation still open: no frontend UI for any of this yet (login screen,
roster-add form, curriculum view) — API-only. No auth on the roster-add endpoint.
Co-Authored-By: Claude Sonnet 5 <[email protected]>