Files
pangolin/server/migrations/sqlite/000021_devices_user_scoped_uuid.up.sql
T
wangjia 63d1baeb01 fix(server): devices 唯一键改 (user_id,uuid) —— 同机换账号不再 403 死结(F3,#27)
device_id 按安装持久、跨账号复用;旧全局 UNIQUE(uuid) 使同机第二账号注册永远
403 → ConnectNode 判 DEVICE_NOT_REGISTERED,提示的「重新登录」无法自救。

- migration 21(sqlite/mysql):UNIQUE(uuid)→UNIQUE(user_id,uuid);platform 放行
  linux(normalizePlatform 早已接受,旧 CHECK/ENUM 会拒)。SQLite 表重建用
  rename→重建→复制→drop 次序,单事务内不触发 sessions 的级联清空(FK ON)。
- 查找全部收口为按 (user,uuid) 作用域(重复 uuid 跨用户后全局查询歧义):
  findDeviceByUserUUIDTx / FindByUserUUID;Register 删跨用户 Forbidden 分支;
  Delete/ForceLogout/Rename 对他人设备返回 404(不可见);SessionActive 删
  「非本人 fail-safe」分支,dev==nil→false 语义不变。
- 测试:SQLite 真迁移库 F3 回归(两账号同 uuid 各自成行/同用户重复拒/linux 入库/
  sessions 重建后级联仍成立)+ MySQL 集成测试 schema 同步与双账号用例。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:16:40 +08:00

69 lines
3.1 KiB
SQL
Raw 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.
-- 设备唯一键 UNIQUE(uuid) → UNIQUE(user_id, uuid)F3:同机换账号 403 死结)
--
-- device_id 是客户端按「安装」生成并持久的,跨账号复用;全局 UNIQUE(uuid) 使同一台
-- 机器登第二个账号时 RegisterIfAbsent 永远 403 → ConnectNode 判 DEVICE_NOT_REGISTERED,
-- 且提示的「重新登录」无法自救。改为按用户隔离:同一物理设备在每个账号下各有一行。
-- 顺手落地 migration 16 头注释里推迟的两件事:本唯一键改造 + platform CHECK 加 linux
-- (normalizePlatform 已接受 linux,但旧 CHECK 会拒 INSERT)。
--
-- SQLite 无法删除列级 UNIQUE,须重建表。sessions 对 devices 有 ON DELETE CASCADE 外键
-- 且连接开启 foreign_keys=ON:直接 DROP devices 会级联清空全部会话(=全员被登出)。
-- 安全次序(单事务内成立,不依赖 PRAGMA foreign_keys=OFF):
-- ① RENAME devices → devices_old(sessions 的 FK 定义随 rename 跟走到 devices_old);
-- ② 建新 devices(复合唯一键),按原 id 复制数据;
-- ③ RENAME sessions → sessions_old,建新 sessions(FK 指向新 devices),复制数据;
-- ④ 先 DROP 子表 sessions_old,再 DROP 已无引用的 devices_old;重建全部索引。
ALTER TABLE devices RENAME TO devices_old;
CREATE TABLE devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT NOT NULL,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
platform TEXT NOT NULL CHECK (platform IN ('ios','android','windows','macos','linux')),
last_seen DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
client_version TEXT NULL,
totp_trusted_until DATETIME NULL,
dp_uuid TEXT NULL,
UNIQUE (user_id, uuid),
FOREIGN KEY (user_id) REFERENCES users(id)
);
INSERT INTO devices (id, uuid, user_id, name, platform, last_seen, created_at,
client_version, totp_trusted_until, dp_uuid)
SELECT id, uuid, user_id, name, platform, last_seen, created_at,
client_version, totp_trusted_until, dp_uuid
FROM devices_old;
ALTER TABLE sessions RENAME TO sessions_old;
CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
device_id INTEGER NOT NULL,
refresh_jti TEXT NOT NULL UNIQUE,
client_ip TEXT NULL,
client_version TEXT NULL,
created_at DATETIME NOT NULL,
last_active DATETIME NULL,
revoked_at DATETIME NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE
);
INSERT INTO sessions (id, user_id, device_id, refresh_jti, client_ip, client_version,
created_at, last_active, revoked_at)
SELECT id, user_id, device_id, refresh_jti, client_ip, client_version,
created_at, last_active, revoked_at
FROM sessions_old;
DROP TABLE sessions_old;
DROP TABLE devices_old;
CREATE INDEX idx_devices_user ON devices (user_id);
CREATE UNIQUE INDEX idx_devices_dp_uuid ON devices (dp_uuid);
CREATE INDEX idx_sessions_user ON sessions (user_id);
CREATE INDEX idx_sessions_device ON sessions (device_id);