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,20 @@
|
||||
-- instructors — replaces the single shared ADMIN_PASSWORD (Phase 2B) with real per-person
|
||||
-- accounts. password_hash is "<salt-hex>:<hash-hex>" from scrypt (see src/auth/password.js),
|
||||
-- NULL only if an instructor is ever external-auth-only (future OIDC, see auth_identities).
|
||||
--
|
||||
-- Roles are a plain CHECK, not a lookup table — three roles, not expected to churn often, and
|
||||
-- a CHECK is enough to keep bad data out without a join on every permission check.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS instructors (
|
||||
instructor_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT NOT NULL UNIQUE, -- always stored lowercased by the application
|
||||
display_name TEXT NOT NULL,
|
||||
password_hash TEXT,
|
||||
role TEXT NOT NULL DEFAULT 'instructor' CHECK (role IN ('owner', 'admin', 'instructor')),
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_login_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_instructors_role_status ON instructors(role, status);
|
||||
Reference in New Issue
Block a user