- 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.
132 lines
8.3 KiB
JavaScript
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`),
|
|
}
|