3f7664ef9b
A readers table appears, keyed by Discord user ID and carrying the SHA-256 of the owner's userscript token (the global API token today). Startup seeds exactly one Reader from OWNER_DISCORD_ID, idempotently, and a run-once migration (0004, version-table-gated) attaches existing bookmarks to it before reshaping: the surrogate key column is dropped and bookmarks are keyed (reader_id, site, series_id) with an FK to readers ON DELETE CASCADE, so a duplicate bookmark for one Reader and Series is impossible at the database level. Every store read and write is now scoped to the reader it names; handlers act as the seeded owner while the global token remains the only credential. Authentication and the wire format are untouched: the flat JSON still carries key/site/series_id, with key derived on read. OWNER_DISCORD_ID is a new required env var (compose + docs updated).
18 lines
891 B
SQL
18 lines
891 B
SQL
-- One row per person. Keyed by their Discord user ID; carries the SHA-256 of
|
|
-- their userscript token and when they were created. Hashed because a token
|
|
-- in the database is a token anyone with the database can replay; SHA-256 is
|
|
-- enough because the tokens are high-entropy random values with nothing to
|
|
-- brute-force. No one can register yet, so this table holds exactly the one
|
|
-- owner row the seed creates at startup (see Store.Open).
|
|
CREATE TABLE readers (
|
|
id bigserial PRIMARY KEY,
|
|
discord_id text NOT NULL UNIQUE,
|
|
token_sha256 bytea NOT NULL UNIQUE,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Every bookmark now belongs to a reader. Added nullable: rows created before
|
|
-- this migration have no owner yet — 0004 attaches them to the seeded owner
|
|
-- before NOT NULL and the composite key land.
|
|
ALTER TABLE bookmarks ADD COLUMN reader_id bigint;
|