- migrations/017: email, login_identifier, password_hash, last_login_at, credential_token_* on students. Fully additive, both nullable/independent, no destructive change. - students-auth.js: shared credential/token lifecycle (issue/consume/find), reusing instructors' exact scrypt password.js verbatim. - student-auth-routes.js: /api/student/login, /activate, /change-password, /request-password-reset (anti-enumeration, dev-only token echo), /reset-password, /profile. - metrics.js: hand-rolled /metrics Prometheus exporter, login success/failure counters, no student/instructor ID ever used as a label value. - migrations/018: classroom_sessions/participants/events/announcements/help_requests, with a partial unique index enforcing at most one live classroom per cohort at the DB level.
44 lines
2.8 KiB
SQL
44 lines
2.8 KiB
SQL
-- Real student accounts — Phase 5A §2/§3/§7, docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md.
|
|
--
|
|
-- Additive only (check-before-alter, docs/ENGINEERING_SOP.md §6) — every existing pilot student
|
|
-- row stays a valid academic record with all columns NULL/default. A pilot student
|
|
-- (password_hash IS NULL) can still use the original bare student_id login
|
|
-- (POST /api/login, unchanged) until an instructor converts them; a converted/newly-created
|
|
-- student (password_hash IS NOT NULL) must use the new credentialed login
|
|
-- (POST /api/student/login) instead. See src/students-auth.js.
|
|
--
|
|
-- email vs login_identifier (Phase 5A §3): RootNode serves minors who may not have an email
|
|
-- address. Both are nullable, independently unique — an older/self-paced student gets `email`,
|
|
-- a younger instructor-managed student gets `login_identifier` (any safe non-email string the
|
|
-- instructor assigns, e.g. a classroom username). Exactly one of the two is required once an
|
|
-- account is credentialed (enforced in application code, not a CHECK, so the exact rule can
|
|
-- evolve without a migration). Neither column names or implies a parent/guardian relationship —
|
|
-- that's explicitly future work (docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md), not built here;
|
|
-- the schema deliberately doesn't need to change to add it later (a guardian would reference a
|
|
-- student_id, not the other way around).
|
|
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS email TEXT;
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS login_identifier TEXT;
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS password_hash TEXT;
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ;
|
|
|
|
-- The activation/reset token is ONE mechanism serving two purposes (first-login activation and
|
|
-- forgot-password) — both are "a time-limited secure token that lets you set a new password,"
|
|
-- functionally identical, so one column set instead of two parallel ones. Only the hash is
|
|
-- ever stored (crypto.createHash('sha256'), same pattern as sessions.js's token_hash) — the
|
|
-- plaintext token exists only in the instructor's API response / student's activation link,
|
|
-- never persisted, never logged.
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS credential_token_hash TEXT;
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS credential_token_purpose TEXT;
|
|
ALTER TABLE students ADD COLUMN IF NOT EXISTS credential_token_expires_at TIMESTAMPTZ;
|
|
|
|
DO $$ BEGIN
|
|
ALTER TABLE students ADD CONSTRAINT students_credential_token_purpose_check
|
|
CHECK (credential_token_purpose IS NULL OR credential_token_purpose IN ('activation', 'reset'));
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_students_email ON students(email) WHERE email IS NOT NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_students_login_identifier ON students(login_identifier) WHERE login_identifier IS NOT NULL;
|