feat(auth): add per-instructor accounts and local password login
Replaces the single shared ADMIN_PASSWORD (Phase 2B) with real accounts: instructors table (email/display_name/role/status), scrypt password hashing with a fresh random salt PER INSTRUCTOR (Phase 2B used one fixed salt because there was exactly one shared password — that assumption is gone), auth_identities table so a future OIDC login can map to the same instructor_id without a schema change. New POST /api/auth/instructor/login, POST /api/auth/logout, GET /api/auth/session, POST /api/auth/change-password (old password required, revokes other sessions). No account enumeration: identical response whether the email doesn't exist or the password is wrong. bootstrap-owner.js is the only supported way to create the first account — no manual database editing to stand up a fresh deployment. Refuses to run if an active owner already exists unless the target email is an existing instructor (zero-owners recovery path).
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
// Password hashing for instructor accounts — scrypt (Node stdlib, no new dependency) with a
|
||||
// fresh random salt per instructor, stored alongside the hash as "<salt-hex>:<hash-hex>".
|
||||
// Phase 2B's admin-auth.js used one FIXED salt because there was exactly one shared password;
|
||||
// now that there are real per-person accounts, each one gets its own salt.
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
const KEY_LEN = 64;
|
||||
|
||||
function hashPassword(plain) {
|
||||
const salt = crypto.randomBytes(16);
|
||||
const hash = crypto.scryptSync(plain, salt, KEY_LEN);
|
||||
return `${salt.toString('hex')}:${hash.toString('hex')}`;
|
||||
}
|
||||
|
||||
function verifyPassword(plain, stored) {
|
||||
if (!plain || !stored) return false;
|
||||
const [saltHex, hashHex] = stored.split(':');
|
||||
if (!saltHex || !hashHex) return false;
|
||||
const salt = Buffer.from(saltHex, 'hex');
|
||||
const expected = Buffer.from(hashHex, 'hex');
|
||||
const actual = crypto.scryptSync(plain, salt, expected.length || KEY_LEN);
|
||||
if (actual.length !== expected.length) return false;
|
||||
return crypto.timingSafeEqual(actual, expected);
|
||||
}
|
||||
|
||||
module.exports = { hashPassword, verifyPassword };
|
||||
Reference in New Issue
Block a user