-- Ported from the SQLite migration of the same number. Same version, same -- meaning, different spelling: SQLite's storage classes and PostgreSQL's types -- do not line up, and one shared file would be a promise neither could keep. -- -- Times are milliseconds since the epoch in BIGINT, exactly as in SQLite, -- rather than TIMESTAMPTZ. The Go code reads and writes int64 milliseconds -- everywhere; converting here would mean two time representations in one -- codebase, and the conversion would be the thing that is wrong at 2am. -- Accounts, so a person can sign up, be approved, and then issue their own keys -- without an operator running a command for each one. CREATE TABLE users ( id TEXT PRIMARY KEY, -- Lowercased at the boundary. Two accounts differing only in case would be -- two accounts to the database and one to every human who looks at them. email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, -- argon2id, encoded with its own parameters so the cost can be raised later -- without invalidating existing hashes. Slow hashing is right here and wrong -- for API keys: this input is chosen by a human and has a dictionary. password_hash TEXT NOT NULL, -- 'pending' until an operator approves, then 'active'. 'suspended' keeps the -- account and stops it working. state TEXT NOT NULL, -- Assigned at approval: which project this person's keys belong to, and -- therefore which limits they inherit. project_id TEXT REFERENCES projects(id), created_at BIGINT NOT NULL, approved_at BIGINT, approved_by TEXT, last_login_at BIGINT ); CREATE INDEX users_by_state ON users(state); -- Server-side sessions rather than signed cookies, so signing out and -- suspending an account take effect immediately instead of whenever a token -- happens to expire. CREATE TABLE sessions ( token_hash BYTEA PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), -- Per session, submitted with every form. A cookie alone authenticates the -- browser, not the page that asked, which is what makes a cross-site POST -- work. csrf_hash BYTEA NOT NULL, created_at BIGINT NOT NULL, expires_at BIGINT NOT NULL, last_seen BIGINT NOT NULL ); CREATE INDEX sessions_by_user ON sessions(user_id); CREATE INDEX sessions_by_expiry ON sessions(expires_at); -- A key issued through the profile page belongs to the person who made it, so -- they can see and revoke their own and nobody else's. NULL means a key an -- operator issued directly. ALTER TABLE api_keys ADD COLUMN user_id TEXT REFERENCES users(id); CREATE INDEX api_keys_by_user ON api_keys(user_id);