From 53ae90ec13a11a0136e9d93c32f42d22682fa5a8 Mon Sep 17 00:00:00 2001 From: felis Date: Sat, 18 Jul 2026 22:24:00 -0700 Subject: [PATCH] move live source ownership to accounts --- README.md | 5 +- apps/overlay/src/control.tsx | 88 +++++++++++++------ apps/overlay/src/main.tsx | 12 ++- apps/server-rust/README.md | 5 +- .../migrations/006_account_live_source.sql | 26 ++++++ apps/server-rust/src/app.rs | 4 + apps/server-rust/src/auth.rs | 29 ++---- apps/server-rust/src/components.rs | 9 +- apps/server-rust/src/db.rs | 13 ++- apps/server-rust/src/domain.rs | 2 +- apps/server-rust/src/http_api.rs | 11 ++- apps/server-rust/src/live/mod.rs | 6 +- apps/server-rust/src/live/supervisor.rs | 4 +- apps/server-rust/src/realtime.rs | 71 ++++++++------- apps/server-rust/src/repository.rs | 23 ++--- apps/server-rust/src/song_request.rs | 4 +- docs/architecture.md | 12 +-- docs/components/README.md | 10 +-- docs/protocol.md | 4 +- docs/security.md | 5 +- 20 files changed, 219 insertions(+), 124 deletions(-) create mode 100644 apps/server-rust/migrations/006_account_live_source.sql diff --git a/README.md b/README.md index 6966e24..87a83d8 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ adapter 只负责把强类型 Bilibili 命令转换成稳定的领域事件。cr - `components.rs` 注册组件定义、设置 schema/迁移、事件订阅、纯投影和独立副作用 handler;新礼物展示或点歌姬不需要修改弹幕姬队列核心。 - `realtime.rs` 按 component - UUID 建立独立广播通道,路由时同时校验 owner 与 source;副作用 handler 不依赖 OBS + UUID 建立独立广播通道;账户直播事件先按 owner 提供给全部启用组件,再由组件订阅筛选,副作用 handler 不依赖 OBS WebSocket 是否在线。 - `auth.rs`、`repository.rs` 和 PostgreSQL RLS 共同实现身份、凭据、直播源、组件与 token 的用户隔离;`http_api.rs` @@ -116,7 +116,8 @@ https://danmaku.luoxingci.com/control/setup 创建和撤销邀请码。每个邀请码只能使用一次,并在创建时固定绑定一个尚未占用的 Bilibili `room_id`;注册者不能修改该房间,账户创建后房间绑定也不可更改。受邀用户打开注册链接,选择用户名、扫描自己的 TOTP 二维码并确认动态码即可完成注册。普通用户不能创建邀请码。 -每个用户在 `/control/` 配置自己的 CookieCloud 同步 UUID/Key 和密码,地址必须位于部署管理员配置的 +每个用户在 `/control/account` +配置账户级直播监听和自己的 CookieCloud 同步 UUID/Key 与密码。该配置只创建一条上游连接,并供账户下全部现有和未来组件使用。地址必须位于部署管理员配置的 `security.cookiecloud_allowed_hosts` 白名单。服务禁止 HTTP 重定向并安全编码 Key 路径,避免用户凭据导致服务端任意请求。服务会先验证凭据,再将敏感字段按用户独立加密保存;Cookie、Key 和明文密码不会返回浏览器,也不会与其他账户共享。CookieCloud 中需要存在 Bilibili `SESSDATA`。 diff --git a/apps/overlay/src/control.tsx b/apps/overlay/src/control.tsx index 132641f..602b862 100644 --- a/apps/overlay/src/control.tsx +++ b/apps/overlay/src/control.tsx @@ -1,8 +1,8 @@ /** * Authenticated component studio. * - * This module composes tenant-scoped component settings, CookieCloud source - * configuration, isolated test events, OBS token rotation and system-admin + * This module composes tenant-scoped component settings, account-level live + * source configuration, isolated test events, OBS token rotation and system-admin * invitations. One-time secrets remain local to the panel that created them; * update blockers prevent the PWA from refreshing until they are saved. */ @@ -98,7 +98,7 @@ function ControlLayout({ children, }: { user: AuthUser - active: 'components' | 'invitations' + active: 'components' | 'account' | 'invitations' onLogout: () => Promise children: ReactNode }) { @@ -117,6 +117,9 @@ function ControlLayout({ 我的组件 + + 直播账户 + {isAdmin && ( 邀请码 @@ -509,21 +512,17 @@ function SourceEditor({ source: CookieCloudSource onSaved: (source: CookieCloudSource) => void }) { - const [roomId, setRoomId] = useState(source.roomId) + const roomId = source.roomId const [host, setHost] = useState(source.cookieCloud.host) const [key, setKey] = useState(source.cookieCloud.key) const [password, setPassword] = useState('') const [busy, setBusy] = useState(false) const [flash, setFlash] = useState() const sourceDirty = - roomId !== source.roomId || - host.trim() !== source.cookieCloud.host || - key !== source.cookieCloud.key || - Boolean(password) + host.trim() !== source.cookieCloud.host || key !== source.cookieCloud.key || Boolean(password) usePwaUpdateBlocker('live-source', '保存或还原直播源与 CookieCloud 设置', busy || sourceDirty) useEffect(() => { - setRoomId(source.roomId) setHost(source.cookieCloud.host) setKey(source.cookieCloud.key) setPassword('') @@ -535,7 +534,7 @@ function SourceEditor({ setFlash(undefined) try { const payload = await api( - '/api/v1/source', + '/api/v1/account/live-source', json('PUT', { roomId: roomId.trim(), cookieCloud: { @@ -548,7 +547,7 @@ function SourceEditor({ const next = normalizeSource(payload) onSaved(next) setPassword('') - setFlash({ kind: 'success', text: '直播源已保存,连接会使用新的隔离配置。' }) + setFlash({ kind: 'success', text: '账户直播源已保存,所有组件会共享这条事件流。' }) } catch (reason) { setFlash({ kind: 'error', text: errorMessage(reason, '直播源保存失败') }) } finally { @@ -559,7 +558,7 @@ function SourceEditor({ return ( @@ -572,13 +571,7 @@ function SourceEditor({