-- 0003_create_token_tables.up.sql -- -- DB 接入塊 3:建立 pairing_tokens + session_tokens 兩張表(token 分表決策)。 -- 對齊 docs/autoflow/04-architecture/database.md §2.4、§4(token 分表段落)、§5.1。 -- -- 接續 0001(users + models)/ 0002(devices);兩張 token 表的 user_id / device_id -- 都是 REFERENCES 既有表的 FK(users 於 0001、devices 於 0002 建立)。 -- -- 環境事實(與 0001/0002 相同):PostgreSQL 14.23。 -- -- ── 分表決策(database.md §4)────────────────────────────────────────────── -- pairing_tokens 與 session_tokens「分表」,不共表 by kind。理由: -- 1. code 中是兩個獨立 struct + 兩個 Store interface(PairingStore / SessionTokenStore), -- 欄位與方法集不同(pairing 有 used_at 一次性語意、kind;session 有 parent_token_hash、無 used_at)。 -- 2. 共表會讓 used_at / parent_token_hash 對另一類永遠為 NULL,欄位語意混淆。 -- 3. 分表後各表 schema 乾淨、index 各自最佳化,repository 一對一對映 Store。 -- 代價:稽核「pairing→session 升級鏈」需跨表 join(session_tokens.parent_token_hash -- → pairing_tokens.token_hash);可接受(查詢頻率低)。 -- pairing_tokens(短期一次性配對 token;對齊 internal/auth.PairingToken) -- PK = token_hash(sha256(plaintext)):永不存明文 token(security.md §1.3)。 CREATE TABLE pairing_tokens ( token_hash TEXT PRIMARY KEY, -- sha256(plaintext),永不存明文 user_id UUID NOT NULL REFERENCES users(id), device_id UUID REFERENCES devices(id), -- MarkUsed 綁定後才有(nullable) kind TEXT NOT NULL DEFAULT 'pairing', -- 固定 'pairing'(保留欄位,便於觀測/未來擴充) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ, -- 15min TTL;NULL = 永不過期(測試用) used_at TIMESTAMPTZ, -- 一次性:MarkUsed 後 Validate 失敗 revoked_at TIMESTAMPTZ ); -- List by user(UI 顯示),只索引未撤銷紀錄。 CREATE INDEX idx_pairing_tokens_user_active ON pairing_tokens (user_id) WHERE revoked_at IS NULL; -- device_id 反查(cascade 撤銷 by device,塊 5)。 CREATE INDEX idx_pairing_tokens_device ON pairing_tokens (device_id); -- session_tokens(長期可撤銷 tunnel session token;對齊 internal/auth.SessionToken) -- PK = token_hash;device_id 必填(session token 必綁 device)。 CREATE TABLE session_tokens ( token_hash TEXT PRIMARY KEY, -- sha256(plaintext) user_id UUID NOT NULL REFERENCES users(id), device_id UUID NOT NULL REFERENCES devices(id), -- session token 必綁 device parent_token_hash TEXT, -- 升級來源 pairing token 的 hash(稽核鏈,可 join pairing_tokens.token_hash) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expires_at TIMESTAMPTZ, -- 90 天 TTL;NULL = 永不過期 revoked_at TIMESTAMPTZ -- 注意:無 used_at(非一次性)、無 kind ); -- List by user,只索引未撤銷紀錄。 CREATE INDEX idx_session_tokens_user_active ON session_tokens (user_id) WHERE revoked_at IS NULL; -- device_id 反查(cascade 撤銷 by device,塊 5)。 CREATE INDEX idx_session_tokens_device ON session_tokens (device_id); -- parent_token_hash 稽核鏈查詢(join pairing_tokens.token_hash)。 CREATE INDEX idx_session_tokens_parent ON session_tokens (parent_token_hash);