-- 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 INTEGER NOT NULL, approved_at INTEGER, approved_by TEXT, last_login_at INTEGER ); 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 BLOB 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 BLOB NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, last_seen INTEGER 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);