feat(student): wire roster identity and student session
New Platform Shell entry point (frontend/src/platform/) alongside the existing, completely unmodified Lab UI (App.jsx and everything under frontend/src/components/). main.jsx now splits on the current URL path — /lab/<token>/ renders the existing <App/> exactly as before; anything else renders the new <PlatformShell/>. Both live in the same built bundle (it's served both by the persistent identity instance and by every ephemeral Lab container), split purely at runtime. - EntryScreen.jsx — cohort dropdown -> name dropdown (existing GET /api/roster/cohorts, GET /api/roster?cohort=) -> POST /api/login. No password. Visibly labeled pilot identity, not production auth, per the governing requirement that this be stated explicitly, not just true in the architecture docs. - api.js — student_id lives in localStorage only, sent as X-Student-Id on every authenticated request. Every authenticated call throws if no student is stored rather than silently going out anonymously — this is what "prevent accidental anonymous progress writes" means in practice, not just a comment. - PlatformShell.jsx — entry screen if no stored session, dashboard otherwise. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// Platform Shell API client. Student identity is pilot-grade (roster pick-your-name, no
|
||||
// password) — see docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md "Student identity limitation."
|
||||
// student_id lives in localStorage only, never a cookie, never sent anywhere except as the
|
||||
// X-Student-Id header on these specific requests.
|
||||
|
||||
const STORAGE_KEY = 'rootnode_student'
|
||||
|
||||
export function getStoredStudent() {
|
||||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null') } catch { return null }
|
||||
}
|
||||
export function storeStudent(student) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(student))
|
||||
}
|
||||
export function clearStudent() {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
const student = getStoredStudent()
|
||||
// Prevents accidental anonymous progress writes (Phase 2A requirement): every call that
|
||||
// needs identity throws instead of silently going out unauthenticated.
|
||||
if (!student?.student_id) throw new Error('No student session')
|
||||
return { 'X-Student-Id': student.student_id }
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
export const api = {
|
||||
cohorts: () => fetch('/api/roster/cohorts').then(json),
|
||||
roster: (cohort) => fetch(`/api/roster?cohort=${encodeURIComponent(cohort)}`).then(json),
|
||||
login: (student_id) => fetch('/api/login', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ student_id }),
|
||||
}).then(json),
|
||||
|
||||
curriculumPath: () => fetch('/api/curriculum/path', { headers: authHeaders() }).then(json),
|
||||
|
||||
environmentStatus: () => fetch('/api/environment/status', { headers: authHeaders() }).then(json),
|
||||
launchEnvironment: () => fetch('/api/environment/launch', { method: 'POST', headers: authHeaders() }).then(json),
|
||||
resetEnvironment: () => fetch('/api/environment/reset', { method: 'POST', headers: authHeaders() }).then(json),
|
||||
}
|
||||
Reference in New Issue
Block a user