visionA/visionA-backend/migrations/0003_create_token_tables.up.sql
jim800121chen 4d0b870480 feat(visionA-backend): DB 接入 — 6 store 接 PostgreSQL/Redis 持久化(塊 0-5)
把 visionA-backend 6 個 in-memory store 接到資料庫持久化,範圍=完整
(PG 全接 + session 接 Redis + 交易韌性)。interface / handler 不動,
只加 DB 實作 + 換 wiring,config 未設 DB 時保留 in-memory fallback。

- 塊 0 基礎建設:pgx/v5 連線池 + DatabaseConfig/RedisConfig + golang-migrate
  runner(embed)+ cmd/migrate + testcontainers 測試基礎建設
- 塊 1 model → Postgres:array 映射、upsert 保留 CreatedAt、faa_object_key、
  三維 filter(owner/chip/source)、soft-delete partial index
- 塊 2 device → Postgres:partial unique(已刪 serial 可重註冊)、雙狀態欄位
- 塊 3 token → Postgres:pairing_tokens + session_tokens 分表、token_hash 當 PK
- 塊 4 userSession → Redis:idle + absolute 雙 TTL 取代 cleanup goroutine
  (tunnel session 維持 in-memory,yamux handle 不可序列化)
- 塊 5 交易/韌性:WithTx helper + 刪 device cascade 撤銷 token(同 tx 原子)
  + /healthz ping PG/Redis(fail-fast 503)+ pgx error 統一映射(不洩漏 raw error)

降級策略(fail-fast):PG 掉 → 持久資料 API 回 503;Redis 掉 → session 失敗
不自動 fallback in-memory(避免多機 session 不同步)。

DB:PostgreSQL 14.23(gen_random_uuid 內建、無 citext → email 用 lower() unique
index)。每塊經 Reviewer 審查 + 真 PG/Redis testcontainers 全量 dbtest 綠燈,
in-memory fallback 未受影響。

docs: 同步更新 database.md(schema/config/migration 清單)+ api-spec.md
(409/503 錯誤碼、/healthz 新行為、device unpair cascade)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:28:04 +08:00

57 lines
3.7 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 0003_create_token_tables.up.sql
--
-- DB 接入塊 3建立 pairing_tokens + session_tokens 兩張表token 分表決策)。
-- 對齊 docs/autoflow/04-architecture/database.md §2.4、§4token 分表段落、§5.1。
--
-- 接續 0001users + models/ 0002devices兩張 token 表的 user_id / device_id
-- 都是 REFERENCES 既有表的 FKusers 於 0001、devices 於 0002 建立)。
--
-- 環境事實(與 0001/0002 相同PostgreSQL 14.23。
--
-- ── 分表決策database.md §4──────────────────────────────────────────────
-- pairing_tokens 與 session_tokens「分表」不共表 by kind。理由
-- 1. code 中是兩個獨立 struct + 兩個 Store interfacePairingStore / SessionTokenStore
-- 欄位與方法集不同pairing 有 used_at 一次性語意、kindsession 有 parent_token_hash、無 used_at
-- 2. 共表會讓 used_at / parent_token_hash 對另一類永遠為 NULL欄位語意混淆。
-- 3. 分表後各表 schema 乾淨、index 各自最佳化repository 一對一對映 Store。
-- 代價稽核「pairing→session 升級鏈」需跨表 joinsession_tokens.parent_token_hash
-- → pairing_tokens.token_hash可接受查詢頻率低
-- pairing_tokens短期一次性配對 token對齊 internal/auth.PairingToken
-- PK = token_hashsha256(plaintext)):永不存明文 tokensecurity.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 TTLNULL = 永不過期(測試用)
used_at TIMESTAMPTZ, -- 一次性MarkUsed 後 Validate 失敗
revoked_at TIMESTAMPTZ
);
-- List by userUI 顯示),只索引未撤銷紀錄。
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_hashdevice_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 天 TTLNULL = 永不過期
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);