Commit Graph
5 Commits
Author SHA1 Message Date
ced4568 3c7f0ca59c feat(activation): wire notifications into enrollment/auth, real password-reset delivery
- integration-routes.js: accepts student.contact_email, calls notifyForEnrollment on a
  genuinely new enrollment only (never on an idempotent replay — inherits Phase 5B's proven
  enrollment-level idempotency, no separate notification dedup needed).
- admin-routes.js: create-student/convert/reset-password now enqueue the corresponding
  notification using the SAME token already minted synchronously (never mints twice, which
  would silently invalidate the token just handed back in the response). Fixed a real bug:
  request-password-reset would mint a reset token for a never-activated student, which makes
  no sense and would confuse them — now falls through to the generic anti-enumeration response.
- admin-programs-routes.js: manual enrollment triggers the same notification decision as the
  website path.
- student-auth-routes.js: /activate records a metric; /change-password and /reset-password
  both queue a password_changed security notice; /request-password-reset now actually delivers
  email (production still never returns the token — unchanged anti-enumeration/isProduction
  gating, just now with a real send behind it).
- rbac.js: requireStudentRelationshipAccess/checkStudentRelationshipAccess — the notification
  visibility rule (cohort-shared or an enrollment they created), reusable beyond just
  enrollments.
- server.js: wires notification-routes.js and starts the notification worker in main().
- docker-compose.dev.yml: PUBLIC_APP_ORIGIN, EMAIL_FROM_NAME, and a parametrized COOKIE_SECURE
  (was hard-commented-out; needed so e2e-phase5c.js can actually prove production gating).
2026-08-25 14:42:34 -05:00
ced4568 611e1ac4ba feat(enrollment): enrollment lifecycle, entitlement gating, self-paced student API
- enrollment.js: server-only status transitions (pending->active->completed, cancel),
  deriveEnrollmentAccess (pure, derived-at-read-time like classroom.js's derivePresence —
  'expired' is never stored, computed from access_ends_at), maybeCompleteEnrollment
  (idempotent, server-derived from the existing capstone achievement).
- environment-view.js: publicEnvView/launchEnvironmentForStudent extracted out of server.js's
  original /api/environment/launch route body so the new entitlement-gated self-paced launch
  reuses the identical disposable-Lab sequence instead of a second copy — behavior for the
  existing cohort/pilot route is completely unchanged.
- program-status.js: combines an enrollment with real curriculum/progress state into one view
  (access, percent-complete, current stage, deterministic next-activity/resume target) — shared
  by the student and admin surfaces so both show identical information.
- selfpaced-routes.js: GET /api/student/programs(/:id), POST .../launch — entitlement enforced
  server-side before ever touching the Provisioner, never just hidden in the UI.
- rbac.js: requireEnrollmentAccess — a plain instructor may manage an enrollment they created
  or that belongs to a student sharing one of their cohorts; a self-paced enrollment can have
  no cohort at all, so the existing cohort-only checks can't cover it.
- server.js: found and fixed a real bug — POST /api/progress/record's handler predated the
  wrap() convention, so an unexpected internal error (a malformed environment_id) crashed the
  entire process instead of just 500ing that request. Now wrapped + input-validated.
2026-08-25 12:08:12 -05:00
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
ced4568 a8e543234c feat(platform): pagination, admission control, cohort roles, version endpoint, structured assessment editor
- Real server-side pagination (platform/api/src/pagination.js) on the audit
  log (was a flat LIMIT 200), student attempt history (was LIMIT 100), and
  assessment session history (was fully unbounded - the one genuine growth
  risk found). Frontend updated (AuditView, AssessmentsView, StudentsView).
- Admission control: resource-policy.yaml's new (disabled-by-default)
  admission_control.max_concurrent_labs, checked before touching Docker at
  all, returns a clean 503 CAPACITY_EXCEEDED instead of failing mid-provision.
  Relayed as a clean student-facing message, never a raw 500. New
  GET /capacity endpoint feeds the instructor dashboard.
- cohort_instructors.role_in_cohort (existed since Phase 2B, never read) is
  now enforced: teacher vs viewer, rbac.js's
  requireCohortWriteAccess/requireStudentCohortWriteAccess.
- GET /api/version - app version, deployed Git SHA, latest migration as a
  schema-version stand-in.
- Structured assessment authoring UI (AssessmentsView.jsx's PoolSpecEditor)
  replaces the raw-JSON pool_spec textarea, backed by a new
  GET /api/admin/scenario-catalog endpoint. Owner-only raw-JSON fallback
  remains for anything the structured editor doesn't cover.
- NEEDS_ATTENTION_FAIL_THRESHOLD/STALE_DAYS moved to env-var configuration.

Full regression suite (e2e-phase2a/2b/3a/3b, 67/67) re-run clean after
these changes; frontend production build confirmed clean.
2026-08-24 20:15:39 -05:00
ced4568 62726455d5 feat(rbac): enforce owner/admin/instructor permissions across the admin API
Three roles (owner > admin > instructor), enforced server-side on every /api/admin/* route
via reusable middleware (requireOwner/requireAdminOrOwner/requireAnyInstructor) — the
frontend hides controls a role can't use, but that's UX, never the actual boundary. A plain
'instructor' is further scoped to cohorts assigned via the new cohort_instructors table
(requireCohortAccess/requireStudentCohortAccess); owner/admin bypass that check entirely.

Owner-only: instructor account lifecycle, audit log. Admin/owner: cohort/student CRUD,
curriculum assignment. Any instructor role (cohort-scoped for 'instructor'): dashboard,
cohort/student views, Prepare/Destroy Labs, provisioning. Deliberately NOT cohort-scoped:
the raw /api/admin/environments/:id/* routes (no cohort context in a bare environment id
without an extra Provisioner round trip on every call) — a plain instructor still has full
Lab control via the cohort/student-scoped routes instead, which already carry the check.

server.js wires the new persistent-session middleware (studentSession/instructorSession —
re-fetches the instructor row from Postgres every request, so a role change or
deactivation takes effect on the very next request with no separate revoke needed) and adds
a last-resort error handler so a DB error in any async route returns 500 instead of
crashing the process or leaking a stack trace.
2026-08-18 13:36:11 -05:00