feat(admin): add cohort and student management portal
New /admin route (frontend/src/admin/), gated by a separate admin session (scrypt + timingSafeEqual against ADMIN_PASSWORD, no plaintext storage, no new dependency). Covers cohort CRUD + roster + curriculum assignment, and student CRUD + cohort assignment + environment/progress/ attempt views, all under /api/admin/* on the Platform API. main.jsx now does a three-way path split (lab / admin / platform) so the existing, unmodified Lab UI, the student dashboard, and the new admin portal all ship in one Vite bundle without a router dependency.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// Admin API client. Uses the rn_admin 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.
|
||||
|
||||
async function json(res) {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}))
|
||||
throw new Error(body.message || body.error || `Request failed (${res.status})`)
|
||||
}
|
||||
return res.json()
|
||||
}
|
||||
|
||||
function post(path, body) {
|
||||
return fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined }).then(json)
|
||||
}
|
||||
function patch(path, body) {
|
||||
return fetch(path, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(json)
|
||||
}
|
||||
function del(path) {
|
||||
return fetch(path, { method: 'DELETE' }).then(json)
|
||||
}
|
||||
function get(path) {
|
||||
return fetch(path).then(json)
|
||||
}
|
||||
|
||||
export const adminApi = {
|
||||
login: (password) => post('/api/admin/login', { password }),
|
||||
logout: () => post('/api/admin/logout'),
|
||||
whoami: () => get('/api/admin/whoami'),
|
||||
|
||||
dashboard: () => get('/api/admin/dashboard'),
|
||||
|
||||
students: () => get('/api/admin/students'),
|
||||
createStudent: (display_name, cohort_id) => post('/api/admin/students', { display_name, cohort_id }),
|
||||
updateStudent: (id, fields) => patch(`/api/admin/students/${id}`, fields),
|
||||
studentProgress: (id) => get(`/api/admin/students/${id}/progress`),
|
||||
studentAttempts: (id) => get(`/api/admin/students/${id}/attempts`),
|
||||
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 }),
|
||||
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`),
|
||||
}
|
||||
Reference in New Issue
Block a user