feat(student): admin student-account management + CSV import, session hardening
- admin-routes.js: create-with-credentials, /convert, /reset-password, /revoke-sessions, /sessions, /import (transactional CSV import — whole file validated, one Postgres transaction, never accepts a password/hash column from input). Fixed GET /api/admin/students to select an explicit safe column list instead of SELECT * — the new password_hash/credential_token_hash columns must never be exposed through this route. - server.js: /api/login now also blocks on credential_token_hash (not just password_hash) — closes a real window where a student mid-conversion stayed reachable via the old bare-student_id path. studentSession middleware now re-verifies status='active' on every request, not just at session creation, matching instructorSession's existing behavior. - sessions.js: revokeAllStudentSessions, listStudentSessions. - rbac.js: extracted checkCohortAccess/checkCohortWriteAccess as shared core functions so the new classroom/help-request RBAC (next commit) reuses the exact same authorization rule instead of a parallel copy. - student-csv.js + test: hand-rolled parser (no new dependency), unit tested. - prometheus.yml.j2: activated the platform-api scrape job.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
// Fast, isolated unit test for student-csv.js — no Postgres, no HTTP. No framework, matching
|
||||
// project convention (see platform/lab-router/test/run.js).
|
||||
|
||||
const assert = require('assert');
|
||||
const { parseStudentCsv } = require('../src/student-csv');
|
||||
|
||||
function main() {
|
||||
{
|
||||
const r = parseStudentCsv('display_name,email,login_identifier,cohort_id\nAlice,[email protected],,root-fall\nBob,,bob01,root-fall');
|
||||
assert.equal(r.errors.length, 0);
|
||||
assert.equal(r.rows.length, 2);
|
||||
assert.equal(r.rows[0].email, '[email protected]');
|
||||
assert.equal(r.rows[1].login_identifier, 'bob01');
|
||||
console.log('ok — valid file parses');
|
||||
}
|
||||
{
|
||||
const r = parseStudentCsv('email\[email protected]');
|
||||
assert.ok(r.errors.length > 0);
|
||||
console.log('ok — missing display_name column rejected');
|
||||
}
|
||||
{
|
||||
const r = parseStudentCsv('display_name,password\nAlice,hunter2');
|
||||
assert.ok(r.errors.some((e) => e.message.includes('password')));
|
||||
console.log('ok — password column never accepted from a file');
|
||||
}
|
||||
{
|
||||
const r = parseStudentCsv('display_name,email\nAlice,[email protected],extra');
|
||||
assert.ok(r.errors.length > 0);
|
||||
assert.equal(r.rows.length, 0);
|
||||
console.log('ok — a malformed row rejects the whole file (transactional, no partial import)');
|
||||
}
|
||||
{
|
||||
const r = parseStudentCsv('display_name,email\nAlice,[email protected]\nBob,[email protected]');
|
||||
assert.ok(r.errors.some((e) => e.message.includes('duplicate email')));
|
||||
console.log('ok — duplicate email within one file rejected');
|
||||
}
|
||||
{
|
||||
const r = parseStudentCsv('display_name,email\nAlice,not-an-email');
|
||||
assert.ok(r.errors.some((e) => e.message.includes('invalid email')));
|
||||
console.log('ok — invalid email format rejected');
|
||||
}
|
||||
console.log('\nAll student-csv unit tests passed.');
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user