Files
rootnode-academy/docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md
T
ced4568 896c230142
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s
docs(phase5a): student identity and remote classroom documentation
- PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md: the full design doc.
- RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md, RUNBOOK_REMOTE_CLASSROOM.md: operational runbooks.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md: closes the 'no real student authentication' gap this
  doc previously flagged as the one item blocking self-paced delivery.
- ENGINEERING_SOP.md §5 rewritten (was describing a pre-Phase-2A state no longer true of any
  deployed version of this codebase) plus a new §14; roadmap item 3 updated.
- LAB_ARCHITECTURE.md, PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md:
  cross-references and a Phase 5A production-validation section.
2026-08-25 01:20:13 -05:00

13 KiB

Phase 5A — Student Identity & Remote Classroom

Status: PHASE 5A COMPLETE. STUDENT IDENTITY COMPLETE. REMOTE CLASSROOM FOUNDATION COMPLETE.

Phase 4.1 closed the last pre-production infrastructure gap without real hardware. Phase 5A's job — still without real hardware — was the last real product gap flagged along the way: pilot students identify themselves by picking a name from a list, and there was no way for an instructor to run a live, remote-capable class through the platform itself. This phase replaces pilot identification with real accounts and adds a remote classroom control center. It does not add video, voice, chat, or any A/V — those stay Meet/Zoom/Teams's job; RootNode owns identity, presence, curriculum guidance, Lab status, help requests, and instructor control.

1. Student identity model

students (migration 017_student_credentials.sql) gained email, login_identifier, password_hash, last_login_at, and a single credential-token triple (credential_token_hash/_purpose/_expires_at) — all nullable, all additive. email and login_identifier are independently unique (partial unique indexes, WHERE ... IS NOT NULL) and both optional: an older/self-paced student gets an email, a younger instructor-managed student gets an instructor-assigned login ID, neither is required, and nothing here blocks a future guardian_id FK for a Parent Portal — deliberately not built this phase (§37).

A student with neither field set is unchanged from before Phase 5A: a plain pilot row, identified with POST /api/login { student_id }. Setting either field (via account creation or /convert) is what makes an account "real." password_hash reuses platform/api/src/auth/ password.js (scrypt, salt:hash) verbatim — the same function instructors use, no second hashing scheme.

2. Credential lifecycle — one token mechanism, not two

Activation ("first login") and password reset are the same problem: a time-limited, single-use secret that lets someone set a new password. students-auth.js's issueCredentialToken/ consumeCredentialToken implement it once (SHA-256-hashed at rest, plaintext returned exactly once in the API response, never logged), with purpose (activation | reset) distinguishing the two callers. Activation tokens live 7 days, reset tokens 1 hour. consumeCredentialToken is atomic: token verification, password hashing, password_hash write, and token-clearing happen in one statement — there is no window where a token is "spent" but the password isn't set.

Instructors never see or choose a student's password (§5's "instructor creates account → temporary credential" flow): POST /api/admin/students, /convert, and /reset-password all return a token (activation or reset), which the instructor relays out of band (printed, read aloud, handed over). The admin UI surfaces the resulting ?activate=<token>/?reset=<token> link once and never again — there is no "view credentials" screen anywhere.

3. Password reset without email delivery

No email-sending infrastructure exists yet, and this phase deliberately doesn't fake one. POST /api/student/request-password-reset always returns an identical generic response regardless of whether the identifier exists (anti-enumeration) and never echoes the token in production. In non-production (isProduction false, i.e. COOKIE_SECURE !== '1') it also returns dev_reset_token so local/CI flows can complete the loop without a real inbox — a documented dev/test path, not a production feature. The realistic production path today is instructor-triggered: POST /api/admin/students/:id/reset-password. Real email delivery is a named, not-yet-built follow-up, not something silently promised.

4. Session and login security — reused, not duplicated

Students and instructors share the exact same sessions table and mechanism (sessions.js: opaque tokens, HttpOnly, SameSite, hashed at rest, revocable), differentiated only by session_type/student_id vs instructor_id. studentSession middleware now re-verifies status = 'active' on every request (previously only checked session existence) — deactivating a student takes effect immediately, matching instructorSession's existing behavior, not just at next login (verified in e2e-phase5a.js).

Rate limiting reuses Phase 4.1's Postgres-backed tooManyAttempts (security.js) with a student: key prefix — 8 attempts / 5 minutes, survives a Platform API restart because it's a table, not an in-memory Map (verified with a real restart in both e2e-phase5a.js and the rehearsal drill). CSRF reuses the existing double-submit cookie pattern (rn_csrf_s).

A real bug this closed: the legacy POST /api/login (bare student_id, no password) only checked password_hash to decide whether an account had "moved on" from pilot mode. An account mid-conversion — instructor has issued an activation token, student hasn't set a password yet — has credential_token_hash set but password_hash still NULL, and was reachable through the old insecure path for that whole window. Found via testing, fixed by checking student.password_hash || student.credential_token_hash instead (server.js).

5. Pilot migration

No destructive migration. 017_student_credentials.sql only adds nullable columns — every existing pilot row, its progress, attempts, achievements, and cohort membership are completely untouched at migration time. POST /api/admin/students/:id/convert assigns email/login_ identifier and issues an activation token without touching student_id (the FK every other table hangs off of) or anything downstream of it. Verified directly, not just asserted: the Phase 5A rehearsal drill (infra/scripts/drills/test-phase5a-rehearsal.js) seeds real pre- existing progress/attempt/achievement rows for a pilot student, converts them, activates them, logs in the new way, and confirms /api/progress shows the exact same completed scenario — nothing orphaned, nothing re-created.

6. Remote classroom — a fourth session concept

Distinct from cohort membership, assessment sessions, and Lab environments: a scheduled, bounded teaching event. 018_classroom_sessions.sql adds classroom_sessions (lifecycle scheduled → live → ended, or cancelled), classroom_participants, classroom_events (append-only audit), classroom_announcements, and help_requests. A partial unique index (WHERE status = 'live') enforces at most one live classroom per cohort at the database level — the app layer catches the resulting 23505 and returns a clean ALREADY_LIVE (409), not a raw constraint error (verified in e2e-phase5a.js).

7. Presence — derived, not stored

classroom.derivePresence(lastSeenAt, leftAt) is a pure function computing online / recently_active / offline from a timestamp at read time — there's no status column that can go stale or need a background job to sweep. Heartbeats (POST /api/classroom/:id/heartbeat, client fires every 20-30s while the Classroom tab is open and joined) are throttled server-side (WHERE last_seen_at IS NULL OR last_seen_at < now() - interval), never depend on Lab terminal activity, and stop the moment the tab is closed or the student leaves — no invasive tracking.

8. The classroom dashboard — real data, not a second state system

GET /api/admin/classroom-sessions/:id is the one place that reaches beyond classroom.js: it joins participant/presence data with the Provisioner's actual environments state (same enum every other Lab-status view uses — none/requested/provisioning/running/resetting/ error/destroying/destroyed) and a lightweight academic snapshot from the existing progress/scenario_attempts tables (current stage, most recent scenario, validation-fail count, hints used). Never inspects terminal command history. Nothing here is a second Lab-state or academic-state system — it's the existing ones, read the same way every other admin view already reads them.

9. Help requests — a queue, not a chat

POST /api/classroom/:id/help-request { category, message } → open → (instructor) acknowledge → resolve. That's the entire surface — explicitly no chat, no video, no remote shell takeover (§37). The instructor dashboard's help queue polls the same dashboard endpoint everything else does.

10. Guidance, never control

setCurrentActivity (current scenario) and announcements are exactly that — a link the student can choose to open, a message they can read. Nothing server-side redirects a student's browser or marks work complete on their behalf. The external meeting URL (classroom_sessions. meeting_url) is a generic field the student sees as a link — RootNode never proxies or embeds Meet/Zoom/Teams/anything else, so it isn't coupled to a single vendor.

11. CSV roster import — whole-file, transactional

student-csv.js hand-parses (no CSV dependency — the file format here is small and simple enough that pulling in a library would just be one more thing to audit) and validates the entire file before any database write. POST /api/admin/students/import then does every insert inside one Postgres transaction — one bad row rolls back the whole import rather than leaving a half-imported roster, which is the more dangerous failure mode of the two. Never accepts a password/password_hash/credential_token_hash column — activation tokens are always generated server-side, never taken from input.

12. Observability

GET /metrics is a hand-rolled Prometheus text-exposition endpoint (no prom-client dependency — a handful of counters/gauges don't need one) with real in-process counters for login success/failure and on-demand gauges for live-classroom/help-request/Lab-state counts. No student or instructor ID ever appears as a label value — every label is a bounded enum (status, presence, result), per the explicit no-PII/no-high-cardinality-labels instruction. The Prometheus scrape job for the Platform API (infra/monitoring/prometheus/prometheus.yml.j2), previously commented out, is now active.

13. Frontend

Student side (frontend/src/platform/): LoginScreen (credentialed, now the default entry point) with links to the legacy pilot entry and forgot-password; SetPasswordScreen (shared activate/reset UI, driven by ?activate=/?reset= query params — no router library, matching this app's existing state-machine-per-shell convention); ForgotPasswordScreen; a Classroom tab (live-class banner, join, guidance-only current-activity link, own Lab status/launch reusing the My Learning tab's exact API, announcements, Need Help form); a Profile tab (identity, cohort, progress, achievements, active-session count, change-password).

Admin side (frontend/src/admin/): StudentsView gained credential status in the roster table, a create-student form (email/login-ID/cohort), convert/reset-password/revoke-sessions controls per student, and a bulk CSV import form. A new ClassroomView is the control center: cohort picker → schedule/list classes → a live dashboard (participants with presence/Lab-status/ current-work, help queue with acknowledge/resolve, current-activity setter, announcement poster, start/end/cancel, Prepare Labs).

14. Testing — real, not asserted

  • platform/test/e2e-phase5a.js (21 checks): activation single-use, password security (§32 — correct/wrong/unknown-identifier/deactivated/immediate-deactivation/rate-limit/rate-limit- survives-restart/change-password/session-revocation/forgot-password anti-enumeration/admin reset/CSRF), and the two-student RBAC matrix (§31 — student can't reach /api/admin/*, assigned/unassigned/viewer instructor access), plus a classroom lifecycle smoke pass.
  • infra/scripts/drills/test-phase5a-rehearsal.js (15 checks): 5 pilot students, real seeded progress/attempt/achievement history, a live classroom, help requests, announcements, a real Platform API restart mid-rehearsal proving classroom/session/presence recovery, then converting two students mid-rehearsal and confirming every bit of their prior history survived under the new login.
  • Both run against a live docker-compose.dev.yml stack over real HTTP, not mocks.
  • The full pre-existing regression suite (Phase 1/2A/2B/3A/3B/4.1) was re-run unmodified given how much shared code (server.js, admin-routes.js, sessions.js, rbac.js) this phase touched — see the Phase 5A completion report for results.

15. What was deliberately not built

Per §37: video/voice conferencing, student-to-student chat, remote instructor shell takeover, screen recording or proctoring, public registration or course catalog, payments/Stripe, a Parent Portal, school/SIS integrations, real email delivery infrastructure, certificates, an AI tutor, a mobile app, a multi-Lab-host scheduler, and production deployment itself. Each is a plausible "while we're in here" addition and each was left alone on purpose — the schema and API surface built this phase don't block any of them later.