// 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`), // ── Phase 5C §15/§16: enrollment operations & communications ──────────────────────────── notifications: (page = 1) => get(`/api/admin/notifications?page=${page}`), studentNotifications: (studentId) => get(`/api/admin/students/${studentId}/notifications`), resendActivation: (studentId) => post(`/api/admin/students/${studentId}/resend-activation`), sendEnrollmentConfirmation: (studentId, enrollmentId) => post(`/api/admin/students/${studentId}/send-enrollment-confirmation`, enrollmentId ? { enrollment_id: enrollmentId } : undefined), }