-- 0012_api_tokens.sql — tokens for non-browser clients. -- -- Jellyfin clients send `X-Emby-Token`, Subsonic clients send `t=`, and neither -- speaks the session cookie the web app uses. Rather than weaken the cookie -- model, these live beside it: a token is a second, revocable way to be a user, -- and every request still resolves to the same auth.User and goes through the -- same grant chokepoint. -- -- Two storage shapes, for a real reason: -- -- * Jellyfin sends the token as a bearer value, so only a hash is needed — -- same reasoning as sessions. A stolen database yields no usable token. -- * Subsonic's `t` parameter is md5(apiKey + salt), and the server must -- compute that to compare. That requires the KEY itself, so a Subsonic key -- is stored in the clear. It is a generated random secret, never a user's -- password, and it is revocable — but it is worth being explicit that this -- column is the one place a usable credential sits in the database. -- -- `token_plain` is therefore empty for Jellyfin and populated for Subsonic. CREATE TABLE api_tokens ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, kind TEXT NOT NULL, -- 'jellyfin' | 'subsonic' label TEXT NOT NULL DEFAULT '', -- shown in the UI, e.g. "Living room TV" token_hash TEXT NOT NULL, -- sha256 hex, used for lookup token_plain TEXT NOT NULL DEFAULT '', -- Subsonic only; see above created_at TEXT NOT NULL DEFAULT '', last_used_at TEXT NOT NULL DEFAULT '', expires_at TEXT NOT NULL DEFAULT '' -- '' means never ); CREATE INDEX idx_api_tokens_hash ON api_tokens(token_hash); CREATE INDEX idx_api_tokens_user ON api_tokens(user_id, kind);