feat(identity): add persistent Postgres-backed session store
Replaces the in-memory studentSessions/adminSessions Maps (Phase 2B — gone on every restart) with a sessions table. Browser holds an opaque CSPRNG bearer token; only its SHA-256 hash is ever stored server-side, same principle as password hashing. Each session has an absolute expiry (fixed at creation) and a sliding idle expiry (pushed forward on every authenticated request, capped at the absolute one) — one atomic UPDATE...RETURNING per resolve, no read-then-write race. Student and instructor sessions stay fully separate token namespaces, matching Phase 2B's cookie-name separation. cookies.js centralizes Set-Cookie construction so the Secure attribute (COOKIE_SECURE=1, for real HTTPS deployments) is applied consistently instead of hand-built per call site.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
-- sessions — replaces Phase 2B's in-memory studentSessions/adminSessions Maps. A Platform API
|
||||
-- restart no longer logs everyone out, and sessions are individually revocable/auditable.
|
||||
--
|
||||
-- Only a SHA-256 hash of the opaque bearer token is stored (token_hash) — never the raw token
|
||||
-- itself, so a database read (backup, dump, compromised replica) can't be turned into working
|
||||
-- session cookies.
|
||||
--
|
||||
-- expires_at is the absolute ceiling (set once, at creation). idle_expires_at slides forward on
|
||||
-- every authenticated request (see src/sessions.js resolve*Session), capped at expires_at —
|
||||
-- whichever of the two is hit first ends the session. session_type distinguishes student vs.
|
||||
-- instructor sessions; the CHECK below keeps a session row from ever being ambiguous about
|
||||
-- which kind it is, so a bug elsewhere can't accidentally treat one as the other.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_type TEXT NOT NULL CHECK (session_type IN ('student', 'instructor')),
|
||||
student_id TEXT REFERENCES students(student_id),
|
||||
instructor_id UUID REFERENCES instructors(instructor_id),
|
||||
token_hash TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
idle_expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
metadata JSONB,
|
||||
CHECK (
|
||||
(session_type = 'student' AND student_id IS NOT NULL AND instructor_id IS NULL) OR
|
||||
(session_type = 'instructor' AND instructor_id IS NOT NULL AND student_id IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_instructor ON sessions(instructor_id) WHERE instructor_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_student ON sessions(student_id) WHERE student_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expiry ON sessions(expires_at);
|
||||
Reference in New Issue
Block a user