// Phase 5B §6/§7/§8/§9 — student-facing self-paced program API. Only ever shows a student // programs THEY hold an enrollment for (never a public catalog — see §22, no public course // catalog this phase) and enforces access/launch entitlement server-side, not just by hiding a // button (§9: "Hiding a button in React is not access control"). const enrollment = require('./enrollment'); const { buildProgramStatus } = require('./program-status'); const { launchEnvironmentForStudent } = require('./environment-view'); module.exports = function registerSelfPacedRoutes(app, deps) { const { studentSession, getPool, curriculum, loadProgress, provisioner, wrap } = deps; const statusDeps = { pool: getPool(), curriculum, loadProgress }; // §6 — "My Programs": every enrollment this student holds, active and historical alike, // annotated with real progress/access/next-activity. The frontend splits active vs completed // client-side from the `status`/`effective_status` fields — no separate endpoint needed. app.get('/api/student/programs', studentSession, wrap(async (req, res) => { const rows = await enrollment.listEnrollmentsForStudent(getPool(), req.studentId); const views = await Promise.all(rows.map((e) => buildProgramStatus(statusDeps, e))); res.json(views); })); app.get('/api/student/programs/:enrollmentId', studentSession, wrap(async (req, res) => { const row = await enrollment.getEnrollment(getPool(), req.params.enrollmentId); if (!row || row.student_id !== req.studentId) return res.status(404).json({ error: 'not_found' }); res.json(await buildProgramStatus(statusDeps, row)); })); // §8/§9 — the entitlement-gated launch. Delegates to the EXACT same disposable-Lab sequence // /api/environment/launch uses (environment-view.js) — a self-paced Lab is not a different // kind of Lab, just a gated entry point into the same one. The gate itself (deriveEnrollment // Access) is what makes this route different from calling /api/environment/launch directly; // that route is unchanged and still works exactly as before for cohort/pilot students. app.post('/api/student/programs/:enrollmentId/launch', studentSession, wrap(async (req, res) => { const row = await enrollment.getEnrollment(getPool(), req.params.enrollmentId); if (!row || row.student_id !== req.studentId) return res.status(404).json({ error: 'not_found' }); const access = enrollment.deriveEnrollmentAccess(row); if (!access.can_launch) { return res.status(403).json({ error: 'not_entitled', reason: access.reason }); } const { status, body } = await launchEnvironmentForStudent(provisioner, req.studentId); res.status(status).json(body); })); };