Files
Commilitia-Drop/internal/db/migrations/0001_init.sql
T
admin 018a77b13a 登录子系统:OIDC 统一自签 token + 吊销即时生效 + 设备并入会话列表 + 应用内扫码 + 禁则修复
接续 90a3790(扫码 A0+A1+B + step-up 雏形)。蓝本见 AUTH.md。

OIDC 统一到 cdrop 自签 token
- /auth/exchange 先 JWKS 验 id_token 签名(自签 token 的唯一信任锚)再签 cdrop
  自签 HS256 access token(绑 sid),不再把 IdP RS256 下发浏览器;验签失败仅告警
  并回退 RS256(会话仍建、下次 refresh 自愈),登录不中断
- /auth/refresh oidc 路径仍打 IdP 轮换 refresh_token + 探测 IdP 侧吊销,但同样回
  自签 token;createWebSession 改返回行 id
- verify 链保留 RS256 分支仅供桌面端 loopback(其即时吊销留后续)

吊销即时生效 + 被吊销设备自知回退
- verifySelfToken 每请求按 sid 查 web_sessions 行,行删即拒 → 吊销在被吊销设备
  下一次请求即生效(不等 TTL),oidc / self / guest 一致
- 前端仅在「服务端以 401 确证会话失效」时 forceLogout 清登录态、回登录页;短期连接
  失败(5xx / 网络)一律保留登录态(refreshTokens 改三态 refreshed/invalid/transient,
  cookieRefresh 改 discriminated 结果)
- __root 反应式守卫:prod 下失登录态且非认证路由即跳登录页;新增 auth.sessionLost.* 三语

设备列表并入会话列表 + 孤儿自动清除
- GET /api/auth/sessions 统一 web_sessions 与原生客户端设备(桌面 / iOS,自 devices
  登记表呈现,native=true,离线也列);同名不重复列出,排除 shortcut 与无会话 browser
- 吊销网页会话连带删其设备登记(无同名在用会话时);原生「登出」走
  DELETE /api/devices/{name}(同要求 step-up)
- 新增 DeleteOrphanBrowserDevices(browser 型且无存活 web_session),接入设备清扫器(1h)

应用内扫码 + 聚珍崩溃 / CJK 禁则修复
- 登录页内置 getUserMedia + jsqr 扫码,不再外跳系统应用(避免开错浏览器)
- 修聚珍(cjk-autospace)MutationObserver 与 React 重渲染同子树冲突致的 insertBefore
  崩溃:扫码 / 显码 / 批准三状态机页整页跳过聚珍(data-jz-skip)
- 修流动正文未跑 CJK 禁则(jinze 为段落级、须 opt-in):设置 / 快捷指令 / 桌面页一批
  hint 补 data-jz-level="paragraph" + justify,短标签 / 状态仍留文本级

step-up 完善
- auth_time 改可选(Casdoor 不下发,原强制致死循环);stepped_up_at 按会话绑定、
  StepUpMaxAgeSeconds 窗口内不复要求;新增 POST /api/auth/stepup
- 批准页 persist 编进 step-up returnTo 防整页跳转丢失;scope 随档绑定(信任=full /
  仅此次=guest),默认偏安全的「仅此次」

文档与测试
- AUTH.md:折中架构、accounts 薄表、令牌架构、扫码流程、step-up、§4.4 统一会话列表
- 新增 Go 测试:OIDC 自签验证、verifySelfToken 吊销即拒、会话吊销连带删设备、
  统一会话列表(含原生 / 不含 shortcut / 不重复)、孤儿清扫、删设备需 step-up
2026-06-22 11:55:02 +08:00

147 lines
7.0 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.
-- cdrop MVP schema (PROJECT_BRIEF.md §4)
-- 以 IF NOT EXISTS 形式由 internal/db/bootstrap.go 启动期单事务执行。
CREATE TABLE IF NOT EXISTS devices (
user_id TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
last_seen INTEGER NOT NULL,
PRIMARY KEY (user_id, name)
);
CREATE TABLE IF NOT EXISTS shortcut_tokens (
jti TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
label TEXT NOT NULL,
scopes TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
last_used_at INTEGER,
revoked INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS transfer_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
sender_name TEXT NOT NULL,
receiver_name TEXT NOT NULL,
state TEXT NOT NULL,
mode TEXT,
file_name TEXT NOT NULL,
file_size INTEGER NOT NULL,
file_sha256 TEXT,
bytes_transferred INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
finished_at INTEGER,
fail_reason TEXT
);
CREATE TABLE IF NOT EXISTS clipboard_state (
user_id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
content TEXT,
source_device TEXT,
updated_at INTEGER NOT NULL,
-- version 是每用户严格单调递增的版本号(每次写 +1),变更探测 / ETag 用;
-- updated_at 仍是墙钟(TTL / 展示)。既有库由 bootstrap 的幂等 ALTER 补列。
version INTEGER NOT NULL DEFAULT 0,
-- origin_ts 是内容在源设备被复制的时刻(ms)。冲突收敛按它做 LWW——写入仅在
-- origin_ts 严格大于现存时被接受,故延迟/乱序到达的旧复制不会盖掉新的。
origin_ts INTEGER NOT NULL DEFAULT 0
);
-- web_sessions:浏览器端「免重登」。cookie 里只放不透明随机串,本表 id 存其
-- SHA-256,故 DB 单独泄露也换不出可用 cookierefresh_token 以 AES-256-GCM 落盘
-- (密钥在容器环境、不在库内)。仅浏览器使用——桌面端 refresh_token 由 Go keyring
-- 持有、走自有 loopback flow,绝不入此表。device_name 随会话存,使 PWA 存储被清后
-- 开机仍能恢复本机设备名。
CREATE TABLE IF NOT EXISTS web_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
refresh_token TEXT NOT NULL,
device_name TEXT NOT NULL DEFAULT '',
user_agent TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
-- kind 区分会话来源(AUTH.md §2.2):oidc=绑定 IdP refresh_token 的正常浏览器
-- 登录(refresh 时向 IdP 续);selfguestcdrop 自签会话,无 IdP refresh_token
-- refresh 时仅按本行自签 access token、不触 IdP。既有行经 bootstrap 的幂等 ALTER
-- 补列后默认视为 oidc/full,行为不变。
kind TEXT NOT NULL DEFAULT 'oidc',
-- scopefullguest。guest(扫码受限借用设备)服务端强制受限:能收发文件,
-- 但不能改账号、不能再批准别的设备、不能签发长效 token(路由层 requireFullSession 守门)。
scope TEXT NOT NULL DEFAULT 'full',
-- granted_by=扫码批准者的 session id,供审计与「吊销我批准过的借用设备」连带吊销。
granted_by TEXT NOT NULL DEFAULT '',
-- stepped_up_at=本会话最近一次通过 step-up 再认证的 Unix 秒(AUTH.md §6)。在
-- StepUpMaxAgeSeconds 窗口内,敏感动作不再重复要求再认证(避免每次都弹再登录)。
stepped_up_at INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_web_sessions_expires ON web_sessions (expires_at);
-- push_subscriptions:浏览器 / PWA 的 Web Push 订阅端点。当某设备「页面已关闭」
-- (无活的 SSE 连接)时,服务端按此表向其推送系统通知;页面开着时事件仍走 SSE,
-- 前端自弹 toast,不触发 push。id 是 SHA-256(endpoint) 的 hex,使同一浏览器重复
-- 订阅幂等覆盖(endpoint 唯一标识一条推送通道)。p256dh/auth 是 RFC 8291 消息加密
-- 用的客户端公钥与鉴权密钥(base64url)。按 (user_id, device_name) 定位收件设备,
-- 与 hub.SendTo 用 device_name 作 deviceID 一致。桌面端不入此表——它走本地原生通知。
CREATE TABLE IF NOT EXISTS push_subscriptions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
device_name TEXT NOT NULL,
endpoint TEXT NOT NULL,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
user_agent TEXT NOT NULL DEFAULT '',
-- locale 随订阅存:推送文案在服务端渲染(SW 只负责展示收到的 title/body),
-- 故须知道该设备的界面语言;用户切换语言时前端重新订阅会幂等刷新此列。
locale TEXT NOT NULL DEFAULT 'zh-CN',
created_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_device
ON push_subscriptions (user_id, device_name);
-- accountscdrop 侧「薄账户数据层」(AUTH.md §2.1)。身份来源仍是 OAuth provider
-- user_id 继续等于 OIDC sub;本表只存 cdrop 自维护的账户元数据,不含任何凭证(密码 /
-- 第二因子全在 provider)。OIDC exchange 成功后 upsert:捕获 match_key(默认 email
-- claim,仅在管理员开启「迁移标记」时用作跨源关联的 join key)、显示名 / 头像、roles
-- groups claim 缓存,admin 判定用)。
CREATE TABLE IF NOT EXISTS accounts (
user_id TEXT PRIMARY KEY,
match_key TEXT NOT NULL DEFAULT '',
display_name TEXT NOT NULL DEFAULT '',
avatar_url TEXT NOT NULL DEFAULT '',
roles TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
last_login_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_accounts_match_key ON accounts (match_key);
-- login_requests:扫码登录的待批准请求(AUTH.md §2.3、§4)。三方密钥分离——
-- poll_secret 存其 SHA-256(新设备私有、QR 不含、领取会话凭它);approval_code 随 QR
-- 给批准方。短 TTL(默认 120s),consumedexpired 后不可复用,由 reaper 清理。
CREATE TABLE IF NOT EXISTS login_requests (
id TEXT PRIMARY KEY,
poll_secret TEXT NOT NULL,
approval_code TEXT NOT NULL,
status TEXT NOT NULL,
new_device_name TEXT NOT NULL,
new_device_type TEXT NOT NULL,
user_agent TEXT NOT NULL DEFAULT '',
request_ip TEXT NOT NULL DEFAULT '',
approver_user_id TEXT NOT NULL DEFAULT '',
grant_scope TEXT NOT NULL DEFAULT '',
grant_persist TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
approved_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_login_requests_expires ON login_requests (expires_at);