Files
rootnode-academy/platform/api
ced4568 d34ddae458 feat(student): admin student-account management + CSV import, session hardening
- admin-routes.js: create-with-credentials, /convert, /reset-password, /revoke-sessions,
  /sessions, /import (transactional CSV import — whole file validated, one Postgres
  transaction, never accepts a password/hash column from input). Fixed GET
  /api/admin/students to select an explicit safe column list instead of SELECT * — the new
  password_hash/credential_token_hash columns must never be exposed through this route.
- server.js: /api/login now also blocks on credential_token_hash (not just password_hash) —
  closes a real window where a student mid-conversion stayed reachable via the old
  bare-student_id path. studentSession middleware now re-verifies status='active' on every
  request, not just at session creation, matching instructorSession's existing behavior.
- sessions.js: revokeAllStudentSessions, listStudentSessions.
- rbac.js: extracted checkCohortAccess/checkCohortWriteAccess as shared core functions so the
  new classroom/help-request RBAC (next commit) reuses the exact same authorization rule
  instead of a parallel copy.
- student-csv.js + test: hand-rolled parser (no new dependency), unit tested.
- prometheus.yml.j2: activated the platform-api scrape job.
2026-08-25 01:19:19 -05:00
..

RootNode Academy — Platform API (Phase 3B)

Lightweight service: no k3s, no --privileged, no Lab terminal runtime. See docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md for the original split/migration design, docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md for persistent sessions, per-instructor accounts, RBAC, and audit logging, and docs/PHASE3B_LEARNING_AND_ASSESSMENT.md for attempt telemetry, progressive hints, curriculum rules, and the exam/capstone engine — this file is the operational reference.

Configuration reference

Variable Required Meaning
PLATFORM_DB_URL no (defaults to local dev Postgres) Postgres connection string
PROVISIONER_URL no Provisioner base URL
PROVISIONER_SHARED_SECRET yes Internal Provisioner auth — process refuses to start without it
PLATFORM_API_PORT no Listen port, default 4300
FRONTEND_DIST_DIR no Where the built frontend lives — set by the Dockerfile, override for local non-Docker dev
SCENARIOS_DIR / BUNDLES_DIR / CURRICULUM_PATH_FILE no Override the default scenarios/ locations, mainly for local dev outside Docker
STUDENT_SESSION_TTL_HOURS / STUDENT_SESSION_IDLE_HOURS no Default 8h / 2h
INSTRUCTOR_SESSION_TTL_HOURS / INSTRUCTOR_SESSION_IDLE_HOURS no Default 12h / 2h
COOKIE_SECURE no Set to 1 once served over real HTTPS — adds Secure to every session/CSRF cookie
TRUST_PROXY no Set to 1 once fronted by a trusted reverse proxy — honors X-Forwarded-For for req.ip (audit source_ip, login rate limiting)

There is no ADMIN_PASSWORD anymore — instructor accounts live in Postgres (instructors table), created via bootstrap-owner.js for the first one and the admin portal after that.

Local dev

cd platform
echo "PROVISIONER_SHARED_SECRET=$(openssl rand -hex 32)" > .env
docker compose -f docker-compose.dev.yml build
docker compose -f docker-compose.dev.yml up -d

cd api
PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
  node bootstrap-owner.js [email protected] "Your Name" "a-real-password-12-chars-plus"

Everything is reachable through the Gateway at http://localhost:4200 — students at /, instructors at /admin (sign in with the bootstrapped owner). Platform API itself isn't meant to be hit directly by a browser (same boundary as the Provisioner relative to Platform API), though nothing stops you from curling it directly at :4300 for local debugging.

Owner bootstrap

node bootstrap-owner.js <email> <display_name> [password]

Prefer BOOTSTRAP_OWNER_PASSWORD over the positional argument outside a throwaway local test — CLI args can leak into shell history and ps. Refuses to run if an active owner already exists unless the target email belongs to an existing instructor (the intended recovery path if a platform is ever locked out with zero owners: point this at a known instructor account to promote them).

SQLite migration

One-time cutover, run against the real data volume:

docker run --rm -v <identity-data-volume>:/data \
  -e SQLITE_PATH=/data/progress.db -e PLATFORM_DB_URL=postgres://postgres:postgres@<host>:5433/rootnode_platform \
  <platform-api-image> node migrate-sqlite.js

Always backs up the SQLite file first, is safe to re-run, and writes a JSON report — see docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §2/§3 for the full guarantees and rollback procedure. This step is about academic data (students/cohorts/progress) — it has nothing to do with instructor accounts, which are Phase 3A-native and start empty until you bootstrap one.

Tests

platform/test/e2e-phase2a.js (student-flow regression), platform/test/e2e-phase2b.js (the classroom acceptance workflow), platform/test/e2e-phase3a.js (persistent sessions including a real restart, instructor auth, RBAC, cohort scoping, CSRF, audit log), and platform/test/e2e-phase3b.js (attempt telemetry, progressive hints, curriculum/Capstone unlock rules, the exam engine including a real restart, two-student assessment isolation, instructor analytics). All four need the full stack up and OWNER_EMAIL / OWNER_PASSWORD set to a bootstrapped account — see platform/README.md.

Known limitations

See docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md "Known limitations" (sessions are Postgres-backed and survive a restart, but there's no distributed revocation channel to the Gateway's short-lived whoami cache; OIDC/SSO is a documented adapter interface, not a working integration yet) and docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §20 (no automatic abandoned-attempt detection, needs-attention thresholds are fixed constants, pool_spec authoring is raw JSON in the admin UI).