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.
36 lines
2.0 KiB
SQL
36 lines
2.0 KiB
SQL
-- 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);
|