// Instructor account CRUD + the small set of invariants that protect the platform from // locking itself out (never zero active owners). Shared by the auth routes (login/session) and // the admin instructor-management routes. const { hashPassword } = require('./auth/password'); function publicInstructor(row) { if (!row) return null; const { password_hash, ...rest } = row; return rest; } async function getActiveInstructor(pool, instructorId) { const { rows } = await pool.query( `SELECT * FROM instructors WHERE instructor_id = $1 AND status = 'active'`, [instructorId] ); return rows[0] || null; } // Returns the row regardless of status — the login flow needs to tell "wrong password" apart // from "correct password, but account is deactivated" (see instructor-auth-routes.js), which // requires seeing inactive accounts too. async function getInstructorByEmail(pool, email) { const { rows } = await pool.query(`SELECT * FROM instructors WHERE email = $1`, [String(email).toLowerCase().trim()]); return rows[0] || null; } async function listInstructors(pool) { const { rows } = await pool.query(`SELECT * FROM instructors ORDER BY created_at`); return rows.map(publicInstructor); } async function countActiveOwners(pool, excludingId) { const { rows } = await pool.query( `SELECT count(*)::int AS n FROM instructors WHERE role = 'owner' AND status = 'active' AND instructor_id != $1`, [excludingId || '00000000-0000-0000-0000-000000000000'] ); return rows[0].n; } async function createInstructor(pool, { email, displayName, role, password }) { const emailLower = String(email).toLowerCase().trim(); const passwordHash = hashPassword(password); const client = await pool.connect(); try { await client.query('BEGIN'); const { rows } = await client.query( `INSERT INTO instructors (email, display_name, password_hash, role) VALUES ($1, $2, $3, $4) RETURNING *`, [emailLower, displayName.trim(), passwordHash, role || 'instructor'] ); const instructor = rows[0]; await client.query( `INSERT INTO auth_identities (instructor_id, provider, provider_subject, provider_email) VALUES ($1, 'local', $2, $3)`, [instructor.instructor_id, String(instructor.instructor_id), emailLower] ); await client.query('COMMIT'); return instructor; } catch (e) { await client.query('ROLLBACK'); throw e; } finally { client.release(); } } // Guards the one invariant that matters: never let the platform end up with zero active // owners. Called before any update that would deactivate an owner or demote their role. async function assertNotLastOwner(pool, instructorId) { const remaining = await countActiveOwners(pool, instructorId); if (remaining === 0) { const err = new Error('Cannot remove the last active owner.'); err.code = 'LAST_OWNER'; throw err; } } module.exports = { publicInstructor, getActiveInstructor, getInstructorByEmail, listInstructors, countActiveOwners, createInstructor, assertNotLastOwner };