Files
rootnode-academy/frontend/src/admin/api.js
T
ced4568 17160da89e feat(admin): Programs area, enrollment management, student enrolled-programs view
- admin-programs-routes.js: program CRUD (admin/owner authoring, all roles can view — matches
  assessment_definitions' convention), scoped enrollment listing, manual enroll, activate/
  cancel (via rbac.js's requireEnrollmentAccess).
- ProgramsView.jsx: cohort-picker-free program list/create, per-program detail with
  enrollment table and a manual-enroll form.
- StudentsView.jsx: student detail gained an 'Enrolled programs' card.
- AdminPortal.jsx: new Programs nav entry.
2026-08-25 12:08:29 -05:00

132 lines
8.3 KiB
JavaScript

// Admin API client. Uses the rn_instructor session cookie exclusively (same-origin, sent
// automatically) — completely separate credential/cookie from student sessions (see
// platform/api/src/sessions.js). No admin data is ever cached client-side beyond the current
// React state; nothing here reads or writes localStorage.
//
// rn_csrf_i is a deliberately non-HttpOnly cookie (double-submit CSRF pattern, mirrored on the
// student side in frontend/src/platform/api.js) — read here and echoed back as a header on
// every state-changing request.
const CSRF_COOKIE = 'rn_csrf_i'
function getCookie(name) {
return document.cookie.split('; ').find((c) => c.startsWith(`${name}=`))?.split('=')[1]
}
async function json(res) {
if (!res.ok) {
const body = await res.json().catch(() => ({}))
const err = new Error(body.message || body.error || `Request failed (${res.status})`)
if (body.row_errors) err.row_errors = body.row_errors // Phase 5A §29 — CSV import row-level errors
throw err
}
return res.json()
}
function withCsrf(init) {
return { ...init, headers: { ...init.headers, 'X-CSRF-Token': getCookie(CSRF_COOKIE) || '' } }
}
function post(path, body) {
return fetch(path, withCsrf({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined })).then(json)
}
function patch(path, body) {
return fetch(path, withCsrf({ method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })).then(json)
}
function del(path) {
return fetch(path, withCsrf({ method: 'DELETE' })).then(json)
}
function get(path) {
return fetch(path).then(json)
}
export const adminApi = {
// No CSRF token yet at login (nothing authenticated to forge into).
login: (email, password) => fetch('/api/auth/instructor/login', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }),
}).then(json),
logout: () => post('/api/auth/logout'),
session: () => get('/api/auth/session'),
changePassword: (old_password, new_password) => post('/api/auth/change-password', { old_password, new_password }),
dashboard: () => get('/api/admin/dashboard'),
students: () => get('/api/admin/students'),
createStudent: (fields) => post('/api/admin/students', fields),
updateStudent: (id, fields) => patch(`/api/admin/students/${id}`, fields),
// ── Phase 5A §4-7/§29: credential lifecycle + CSV import ────────────────────────────────
convertStudent: (id, fields) => post(`/api/admin/students/${id}/convert`, fields),
resetStudentPassword: (id) => post(`/api/admin/students/${id}/reset-password`),
revokeStudentSessions: (id) => post(`/api/admin/students/${id}/revoke-sessions`),
studentSessions: (id) => get(`/api/admin/students/${id}/sessions`),
importStudents: (csv) => post('/api/admin/students/import', { csv }),
studentProgress: (id) => get(`/api/admin/students/${id}/progress`),
studentAttempts: (id, page = 1) => get(`/api/admin/students/${id}/attempts?page=${page}`), // Phase 4.1 §16 — paginated, see pagination.js
studentEnvironment: (id) => get(`/api/admin/students/${id}/environment`),
provisionStudentLab: (id) => post(`/api/admin/students/${id}/environment/provision`),
assignStudentCohort: (id, cohort_id) => post(`/api/admin/students/${id}/cohort`, { cohort_id }),
removeStudentCohort: (id, cohortId) => del(`/api/admin/students/${id}/cohort/${cohortId}`),
cohorts: () => get('/api/admin/cohorts'),
createCohort: (fields) => post('/api/admin/cohorts', fields),
updateCohort: (id, fields) => patch(`/api/admin/cohorts/${id}`, fields),
cohortStudents: (id) => get(`/api/admin/cohorts/${id}/students`),
cohortProgress: (id) => get(`/api/admin/cohorts/${id}/progress`),
assignCohortCurriculum: (id, curriculum_path) => post(`/api/admin/cohorts/${id}/curriculum`, { curriculum_path }),
assignCohortInstructor: (id, instructor_id, role_in_cohort) => post(`/api/admin/cohorts/${id}/instructors`, { instructor_id, role_in_cohort }),
prepareLabs: (id) => post(`/api/admin/cohorts/${id}/prepare-labs`),
destroyLabs: (id) => post(`/api/admin/cohorts/${id}/destroy-labs`),
resetEnvironment: (id) => post(`/api/admin/environments/${id}/reset`),
destroyEnvironment: (id) => del(`/api/admin/environments/${id}`),
environmentEvents: (id) => get(`/api/admin/environments/${id}/events`),
instructors: () => get('/api/admin/instructors'),
createInstructor: (fields) => post('/api/admin/instructors', fields),
instructor: (id) => get(`/api/admin/instructors/${id}`),
updateInstructor: (id, fields) => patch(`/api/admin/instructors/${id}`, fields),
revokeInstructorSessions: (id) => post(`/api/admin/instructors/${id}/revoke-sessions`),
resetInstructorPassword: (id, new_password) => post(`/api/admin/instructors/${id}/reset-password`, { new_password }),
auditEvents: (page = 1) => get(`/api/admin/audit-events?page=${page}`), // Phase 4.1 §16 — paginated, see pagination.js
// ── Phase 3B: learning analytics ────────────────────────────────────────────────────────
cohortLearning: (id) => get(`/api/admin/cohorts/${id}/learning`),
studentLearning: (id) => get(`/api/admin/students/${id}/learning`),
scenarioAnalytics: (cohortId) => get(`/api/admin/scenario-analytics${cohortId ? `?cohort_id=${cohortId}` : ''}`),
needsAttention: (cohortId) => get(`/api/admin/cohorts/${cohortId}/needs-attention`),
scenarioCatalog: () => get('/api/admin/scenario-catalog'), // Phase 4.1 §18 — structured assessment editor
// ── Phase 3B: assessment (exam + capstone) management ───────────────────────────────────
assessments: () => get('/api/admin/assessments'),
createAssessment: (fields) => post('/api/admin/assessments', fields),
updateAssessment: (id, fields) => patch(`/api/admin/assessments/${id}`, fields),
assignAssessment: (id, cohort_id) => post(`/api/admin/assessments/${id}/assign`, { cohort_id }),
assessmentSessions: (id, page = 1) => get(`/api/admin/assessments/${id}/sessions?page=${page}`), // Phase 4.1 §16 — paginated, see pagination.js
cancelAssessmentSession: (sessionId) => post(`/api/admin/assessment-sessions/${sessionId}/cancel`),
// ── Phase 5A §14/§21: remote classroom control center ───────────────────────────────────
classrooms: (cohortId) => get(`/api/admin/cohorts/${cohortId}/classroom-sessions`),
createClassroom: (cohortId, fields) => post(`/api/admin/cohorts/${cohortId}/classroom-sessions`, fields),
classroomDashboard: (id) => get(`/api/admin/classroom-sessions/${id}`),
startClassroom: (id) => post(`/api/admin/classroom-sessions/${id}/start`),
endClassroom: (id) => post(`/api/admin/classroom-sessions/${id}/end`),
cancelClassroom: (id) => post(`/api/admin/classroom-sessions/${id}/cancel`),
prepareClassroomLabs: (id) => post(`/api/admin/classroom-sessions/${id}/prepare-labs`),
setClassroomActivity: (id, scenario_id) => post(`/api/admin/classroom-sessions/${id}/activity`, { scenario_id }),
postClassroomAnnouncement: (id, message) => post(`/api/admin/classroom-sessions/${id}/announcements`, { message }),
acknowledgeHelpRequest: (id) => post(`/api/admin/help-requests/${id}/acknowledge`),
resolveHelpRequest: (id, note) => post(`/api/admin/help-requests/${id}/resolve`, { note }),
// ── Phase 5B §11: programs and enrollment ───────────────────────────────────────────────
programs: () => get('/api/admin/programs'),
program: (id) => get(`/api/admin/programs/${id}`),
createProgram: (fields) => post('/api/admin/programs', fields),
updateProgram: (id, fields) => patch(`/api/admin/programs/${id}`, fields),
programEnrollments: (id) => get(`/api/admin/programs/${id}/enrollments`),
createEnrollment: (fields) => post('/api/admin/enrollments', fields),
enrollment: (id) => get(`/api/admin/enrollments/${id}`),
activateEnrollment: (id) => post(`/api/admin/enrollments/${id}/activate`),
cancelEnrollment: (id) => post(`/api/admin/enrollments/${id}/cancel`),
studentEnrollments: (studentId) => get(`/api/admin/students/${studentId}/enrollments`),
}