ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SpacetimeDB 协作画布私有与分享机制实战:基于 Paint App 的私有画布与共享链接实现解析

SpacetimeDB 协作画布私有与分享机制实战:基于 Paint App 的私有画布与共享链接实现解析 SpacetimeDB 协作画布私有与分享机制实战基于 Paint App 的私有画布与共享链接实现解析【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB导读在基于 SpacetimeDB 构建的实时协作画布应用Paint App中私有画布与分享Private Canvases Sharing是访问控制Access Control能力的核心体现画布默认私有、仅创建者可访问通过生成带权限的分享链接邀请他人加入也可以按用户名邀请特定协作者并支持随时吊销链接。本文以 12_private_canvases.md 功能规格为主线结合仓库中完整的 TypeScript SpacetimeDB 参考实现schema.ts 与 index.ts逐项拆解数据模型、reducer 调用链与前端交互帮助你掌握在 SpacetimeDB 上实现私有 → 分享 → 可吊销完整访问控制链路的落地方法。一、功能规格总览私有画布的六项核心要求根据 12_private_canvases.md该特性模块要求实现以下能力#需求关键行为1画布默认私有只有创建者可以访问其他用户无法直接加入2生成分享链接通过链接允许他人加入画布3链接权限可配置Anyone with link can view可查看或 Anyone with link can edit可编辑4按用户名邀请邀请指定用户被邀请者会在自己的画布列表中看到该画布5吊销分享链接吊销后生成新链接旧链接立即失效6画布列表状态展示列表区分 Private、Shared或展示协作者头像这六项需求在参考实现中并非孤立功能而是建立在第 9 个特性模块实时权限09_realtime_permissions.md之上——后者定义了 owner/viewer/editor 三种角色体系与即时生效的权限变更。私有画布模块本质上是用分享入口链接 邀请把访问控制从创建者独占扩展到受控协作者集合。二、数据模型三张核心表支撑访问控制访问控制是否健壮首先取决于数据模型。参考实现 schema.ts 中与私有画布直接相关的表有三张2.1 Canvas画布主体与分享状态Canvas 表 定义了画布本身其中访问控制相关字段为id: t.u64().primaryKey().autoInc(), ownerIdentity: t.identity(), // 创建者即所有者 name: t.string(), isPrivate: t.bool(), // 是否私有默认 true shareLinkToken: t.string().optional(),// 当前分享令牌undefined 表示未分享 shareLinkPermission: t.string().optional(), // view | edit keepForever: t.bool(), // 是否免自动清理 lastActivityAt: t.timestamp(), createdAt: t.timestamp(),要点解析ownerIdentity是所有者的唯一事实来源后续所有权限校验生成链接、吊销链接、改角色、移除成员都以它为准isPrivateshareLinkToken组合描述画布的分享状态私有时shareLinkToken为undefined一旦生成分享链接isPrivate会被置为falseshareLinkPermission取值view或edit直接决定通过链接加入者获得viewer还是editor角色见下文join_canvas_via_link。2.2 CanvasMember成员关系与角色CanvasMember 表 记录谁在哪些画布上、以什么角色存在并为按画布与按用户两种查询分别建立了 btree 索引id: t.u64().primaryKey().autoInc(), canvasId: t.u64(), // 索引canvas_member_canvas_id userIdentity: t.identity(), // 索引canvas_member_user_identity role: t.string(), // owner | editor | viewer invitedAt: t.timestamp(),两个索引分别服务于两类高频查询按画布列出所有成员协作面板、角色管理以及按用户列出其全部画布首页画布列表即需求 4 中被邀请者在画布列表中看到该画布的数据来源。2.3 User用户名邀请的落点user 表 保存identity主键、displayName与avatarColor。由于 SpacetimeDB 的身份体系以 identity 为主键按用户名邀请在实现上通常是先按displayName查找对应用户行再把其identity写入CanvasMember。三、reducer 调用链从私有到分享再到吊销的完整闭环参考实现 index.ts 用 5 个核心 reducer 覆盖了规格中的全部行为下面按生命周期顺序逐一拆解。3.1 create_canvas私有默认的起点create_canvas 是访问控制的源头它一次性完成三件事spacetimedb.reducer(create_canvas, { name: t.string() }, (ctx, { name }) { if (!name.trim()) throw new SenderError(Canvas name cannot be empty); const canvas ctx.db.canvas.insert({ id: 0n, ownerIdentity: ctx.sender, name: name.trim(), isPrivate: true, // ← 默认私有只有创建者可访问 shareLinkToken: undefined, shareLinkPermission: undefined, keepForever: false, lastActivityAt: ctx.timestamp, createdAt: ctx.timestamp, }); // 把创建者登记为 owner 成员 ctx.db.canvasMember.insert({ id: 0n, canvasId: canvas.id, userIdentity: ctx.sender, role: owner, invitedAt: ctx.timestamp }); // 创建默认图层 调度清理任务 ... });三个关键设计isPrivate: true硬编码为创建时的默认值——规格第 1 条画布默认私有在数据层直接落地不依赖客户端传参创建者自动成为role: owner的成员——与 09_realtime_permissions.md 中Canvas creator is the owner的规格一一对应创建时同步生成默认图层并调用scheduleCleanupCheck调度清理检查说明创建画布是一个原子性的多表操作。3.2 join_canvas私有画布的准入校验join_canvas 是私有性在加入路径上的强制关卡if (!isMember canvas.isPrivate) { throw new SenderError(Canvas is private); }也就是说非成员用户在画布仍为私有时直接加入会被拒绝服务端返回SenderError(Canvas is private)。这条校验说明私有性不是纯 UI 层面的隐藏而是 SpacetimeDB reducer 内的服务端强制约束——即便客户端绕过界面直接调用 reducer 也无法进入私有画布。3.3 generate_share_link / join_canvas_via_link链接的生成与使用生成链接generate_share_link 接收canvasId与permissionview | editspacetimedb.reducer( generate_share_link, { canvasId: t.u64(), permission: t.string() }, (ctx, { canvasId, permission }) { const canvas ctx.db.canvas.id.find(canvasId); if (!canvas) throw new SenderError(Canvas not found); if (canvas.ownerIdentity.toHexString() ! ctx.sender.toHexString()) { throw new SenderError(Only the owner can generate share links); } const token generateToken(ctx); ctx.db.canvas.id.update({ ...canvas, shareLinkToken: token, shareLinkPermission: permission, isPrivate: false, // 生成链接即退出私有态 }); } );这里有两个值得注意的服务端约束只有 owner 能生成链接ownerIdentity与ctx.sender不一致直接抛错生成链接的副作用是isPrivate被置为false画布从私有转为共享状态——这正是画布列表状态标识需求 6的数据基础。使用链接join_canvas_via_link 是分享链接的消费端spacetimedb.reducer( join_canvas_via_link, { shareLinkToken: t.string() }, (ctx, { shareLinkToken }) { let canvas null; for (const c of ctx.db.canvas.iter()) { if (c.shareLinkToken shareLinkToken) { canvas c; break; } } if (!canvas) throw new SenderError(Invalid or expired share link); // 已是成员则跳过 ... if (!isMember) { const role canvas.shareLinkPermission edit ? editor : viewer; ctx.db.canvasMember.insert({ id: 0n, canvasId: canvas.id, userIdentity: ctx.sender, role, invitedAt: ctx.timestamp, }); } } );权限映射逻辑非常直接链接权限为edit时加入者获得editor角色否则一律为viewer与规格第 3 条完全一致。加入动作在服务端自动写入CanvasMember行客户端无需额外调用邀请接口。3.4 revoke_share_link吊销即失效revoke_share_link 实现规格第 5 条ctx.db.canvas.id.update({ ...canvas, shareLinkToken: undefined, // 旧令牌被清除 shareLinkPermission: undefined, isPrivate: true, // 画布回到私有态 });吊销的语义是把shareLinkToken置为undefined。由于join_canvas_via_link通过遍历比对c.shareLinkToken shareLinkToken查找画布旧令牌被清除后立即无法匹配等价于旧链接作废。下一次再调用generate_share_link会由generateToken(ctx)生成全新令牌形成旧链接失效、新链接可用的完整循环。值得注意的是吊销分享链接不会移除已通过旧链接加入的成员成员关系仍保留在CanvasMember中它关闭的是未来通过链接进入的通道。3.5 set_member_role / remove_member链接之外的角色治理与分享直接配套的是成员角色治理继承自 09_realtime_permissions.md 并在分享场景复用set_member_role仅 owner 可调用且禁止修改自己的角色用于把viewer/editor互相转换变更即时生效remove_member仅 owner 可调用删除成员行并顺带清理其 presence 与 cursor实现踢出画布的即时生效。这两者共同保证了即便用户通过链接进入了画布owner 仍能对其角色进行事后治理形成链接进入 角色治理 链接吊销三位一体的访问控制闭环。四、前端实现ShareModal 与画布列表状态展示参考实现的客户端client/src/App.tsx提供了与上述 reducer 一一对应的 UI。4.1 ShareModal链接生成、复制与吊销ShareModal 组件 完整实现了分享交互const shareLink canvas.shareLinkToken ? ${window.location.origin}?join${canvas.shareLinkToken} : null; const handleGenerateLink () { conn.reducers.generateShareLink({ canvasId: canvas.id, permission: sharePermission }); }; const handleRevokeLink () { conn.reducers.revokeShareLink({ canvasId: canvas.id }); }; const handleCopyLink () { ... navigator.clipboard.writeText(shareLink); ... }; const handleChangeRole (memberIdentity, role) conn.reducers.setMemberRole({ canvasId: canvas.id, memberIdentity, role }); const handleRemoveMember (memberIdentity) conn.reducers.removeMember({ canvasId: canvas.id, memberIdentity });UI 结构与规格逐条对应链接已存在时展示只读输入框 Copy 按钮 Anyone with the link canview/edit 提示 Revoke红色危险按钮按钮见 App.tsx#L2301-L2347链接不存在时提供权限下拉框Can View/Can Edit与Generate Share Link按钮见 App.tsx#L2349-L2385成员列表区展示每位成员的头像avatarColor、显示名、角色徽标owner 可以对非 owner 成员执行角色切换与移除操作见 App.tsx#L2392-L2444。链接格式为window.location.origin ?joinshareLinkToken客户端在启动时检测 URL 中的join参数并调用conn.reducers.joinCanvasViaLink({ shareLinkToken: joinToken })见 App.tsx#L244实现点链接直接进画布的用户路径。4.2 画布列表Private / Shared / 协作者可视化画布列表渲染 对应规格第 6 条{canvas.isPrivate ( span style{{ color: var(--text-muted) }}/span )}当前参考实现中私有画布在列表项右侧显示 锁图标作为私有标识而Shared标识与协作者头像则基于canvas.shareLinkToken是否有分享链接与canvasMembers.filter(m m.canvasId canvas.id)的成员数量组合推导。列表元数据行同时展示成员数与Today / Yesterday / N days ago的活动时间与活动感知模块保持一致。从实现结构看私有与共享状态的完整可视化可以这样组织Privatecanvas.isPrivate true仅 owner 可见该画布显示锁标识Sharedcanvas.isPrivate false已有分享链接可在列表显示共享标识协作者头像从canvasMembers与users关联后渲染成员头像堆叠成员渲染逻辑见 App.tsx#L2393-L2444 的头像与用户信息关联方式。五、功能在整套 Prompt 体系中的定位该特性并非孤立模块。根据 prompts/README.md 的说明Paint App 的 16 个功能模块按**累积式cumulative**组织每个级别包含之前所有特性Level名称新增功能08permissions权限owner/viewer/editor、即时生效10activity活动流11sharing 私有画布与分享12full 画布聊天、自动清理、文本便签、快捷键因此阅读本模块时应将其视为建立在 Permissions09_realtime_permissions.md角色体系之上的第 11 级能力viewer/editor角色定义了能做什么私有画布与分享定义了谁能进来。两者叠加才是完整的访问控制。完整的累积提示词含本模块在内的全部 12 级功能见 composed/12_full.md 中 Sharing Private Canvases 一节其内容与12_private_canvases.md逐条一致。六、基于源码的工程要点总结从参考实现中可以提炼出以下可复用的工程模式供在 SpacetimeDB 上实现同类私有资源 分享功能时参考私有性必须在服务端强制join_canvas中的if (!isMember canvas.isPrivate) throw表明私有约束属于 reducer 层的数据校验而非仅靠 UI 隐藏这是与 SpacetimeDB 逻辑全部运行在服务端 模型天然契合的做法链接令牌是唯一凭据shareLinkToken作为分享链接的承载字段吊销的本质就是将其置空无需维护黑名单或过期时间表——找不到即无效权限参数在加入时一次性固化join_canvas_via_link根据shareLinkPermission决定角色后写入CanvasMember之后对已有成员的治理完全交给set_member_role/remove_member职责划分清晰owner 身份作为所有敏感操作的前置校验生成/吊销链接、改角色、移除成员四个 reducer 全部以canvas.ownerIdentity ! ctx.sender作为拒绝条件权限控制集中在单一事实源上状态字段驱动 UI 表达isPrivateshareLinkToken两个字段即可推导出 Private/Shared/协作者头像三种列表状态数据模型与界面呈现保持一一映射。若希望进一步验证或扩展该特性可以直接查阅参考实现中的 schema.ts数据模型、index.tsreducer 实现与 client/src/App.tsxShareModal 与画布列表 UI以及配套的 grading_checklist.md 与 grading_rubric.md 评审标准。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表