关于 embed-portal
embed-portal 是用于规划和构建 Domo 嵌入门户的技能,涵盖需求收集、认证、用户与仪表板管理,并将嵌入流程交由专门子技能处理。
主要功能
- 01梳理品牌和嵌入模式需求
- 02规划认证与用户管理
- 03定义动态仪表板目录
- 04委托只读和编辑嵌入流程
- 05衔接可选的客户端交互
适用场景
- 01规划带品牌样式的 Domo 仪表板门户
- 02管理门户用户及仪表板分配
- 03组合只读和可编辑嵌入
- 04为嵌入内容添加筛选和钻取交互
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ stahura/ domo-ai-vibe-rules --skill embed-portal安装后在 Claude Code 中输入/embed-portal确认已加载。
附带文件references/auth-and-users.md
详细介绍
构建 Domo 嵌入门户
本技能运行端到端工作流:先收集需求,再将 Domo 特定实现细节委托给已有的能力技能。不要重复那些技能中的内容——在适当步骤读取它们。
使用的子技能:
programmatic-filters— 只读嵌入令牌流程、服务端过滤器、数据集切换edit-embed— 通过 Identity Broker 编辑嵌入、JWT 认证、Domo 角色jsapi-filters— 客户端过滤、钻取事件、iframe 通信
阶段 1:收集需求
在编写任何代码之前,收集三项信息。以对话方式询问——不要一次性抛出所有问题。如果从上下文中已能明确答案,可跳过。
1. 品牌指南
询问:“门户应使用什么品牌?我需要一个调色板(主色、次要色、强调色)、字体,以及如果有的话,一个 logo。”
如果他们没有什么强烈意见,合理的默认值:
- 主色:
#1A1A2E,次要色:#16213E,强调色:#0F3460,深色上的文本:#E0E0E0 - 字体:Inter(或系统字体栈)
- Logo:使用应用名称的占位符
2. 嵌入模式
询问:“你的用户应该能够仅查看仪表板(只读)、编辑仪表板(在 Domo 中创建/修改内容),还是两者皆可?”
| 模式 | 含义 | 前提条件 |
|---|---|---|
| 只读 | 用户通过嵌入令牌查看经过滤的仪表板 | Domo API 客户端(客户端 ID + 密钥)、嵌入 ID |
| 编辑 | 用户通过 Identity Broker 获得完整的 Domo 编辑会话 | 与 Domo CSM 完成 Identity Broker 设置、JWT 密钥、IDP URL |
| 两者 | 部分仪表板仅可查看,部分可编辑 | 两组前提条件 |
3. 用户管理方式
询问:“你想如何管理门户用户?”
- Domo AppDB(推荐给 Domo 原生团队)——用户、角色和仪表板分配存储在 Domo 的云数据库中。无需外部基础设施。
- 外部数据库(Postgres、MySQL、Supabase 等)——如果你已有基础设施,这是标准做法。
- 第三方认证提供商(Auth0、Clerk、Firebase Auth)——最快的登录设置,开箱即用地处理密码重置/多因素认证。
默认推荐:Domo AppDB——将所有内容保留在 Domo 生态系统中。
阶段 2:搭建应用脚手架
建议使用 Next.js(App Router、TypeScript、Tailwind CSS)——它在一个项目中同时处理 API 层和 UI,保持嵌入令牌流程安全。如果用户偏好其他框架,可以调整——核心模式与框架无关。
项目结构
src/
├── app/
│ ├── api/
│ │ ├── login/route.ts # Login endpoint
│ │ ├── logout/route.ts # Logout endpoint
│ │ ├── users/
│ │ │ ├── route.ts # CRUD users (GET list, POST create)
│ │ │ └── [id]/route.ts # Single user ops (PUT, DELETE)
│ │ ├── me/route.ts # Current user session
│ │ ├── dashboards/
│ │ │ ├── route.ts # Dashboard registry (GET list, POST create)
│ │ │ └── [id]/route.ts # Single dashboard ops (PUT rename, DELETE)
│ │ ├── getembedtoken/route.ts # Read-only embed tokens (if read-only or both)
│ │ ├── editembed/route.ts # Edit-mode JWT (if edit or both)
│ │ └── password-reset/route.ts # Password reset flow
│ ├── login/page.tsx # Login page
│ ├── home/
│ │ ├── layout.tsx # Authenticated layout with sidebar
│ │ ├── page.tsx # Default redirect
│ │ ├── [embedId]/page.tsx # Dashboard embed view
│ │ └── admin/page.tsx # Admin panel (users + dashboards + filters)
│ └── layout.tsx # Root layout with JS API init
├── lib/
│ ├── domoAppDb.ts # AppDB CRUD wrapper (if AppDB chosen)
│ ├── jsapi.ts # Domo JS API handler
│ ├── verifyToken.ts # JWT verification helper
│ └── constants.ts # Config: URLs, collection IDs
├── middleware.ts # Protect routes, check auth
└── tailwind.config.ts # Brand colors/fonts
环境变量
生成 .env.example——只包含与用户选择相关的内容:
# === Domo OAuth (required for read-only embeds) ===
DOMO_CLIENT_ID= # From developer.domo.com > My Account > New Client
DOMO_CLIENT_SECRET=
# === Domo AppDB (if using AppDB for user management) ===
APPDB_TOKEN= # Developer token from Domo > Admin > Auth > Developer Tokens
DOMO_APPDB_BASE_URL= # e.g. https://yourinstance.domo.com
# === Identity Broker (required for edit embeds) ===
IDP_URL= # e.g. https://yourcompany.identity.domo.com
JWT_SECRET= # UUID shared secret from Domo CSM
KEY_ATTRIBUTE= # Routing attribute name (e.g. "keyAttribute")
# === App Auth ===
APP_JWT_SECRET= # Your own secret for login session tokens (separate from Domo's)
阶段 3:构建——实现顺序
按此顺序构建。步骤 1–3 是门户特定的(在此实现)。步骤 4–6 由子技能负责——你的工作是将门户的数据模型接入它们,而不是重新实现它们。
步骤 1:认证与登录
阅读 references/auth-and-users.md 获取完整实现模式。
这是门户特定的——子技能不涵盖它。构建:
- 使用用户品牌的登录页面
- 登录 API 路由:验证凭据,发放会话 JWT 作为 HttpOnly cookie
- 所有受保护路由使用的会话验证辅助函数
- 路由保护中间件
关键决策:
- 密码使用 bcrypt 哈希(10+ 盐轮)
- 会话 JWT 使用
APP_JWT_SECRET签名(你应用自己的密钥,不是 Domo 的JWT_SECRET) - 令牌生命周期 1 小时,HttpOnly + Secure + SameSite=Lax cookie
- 此应用中有两种不同的 JWT:一种用于你的登录会话,一种用于 Domo 编辑嵌入——保持它们分开
第 2 步:用户管理
阅读 references/auth-and-users.md 获取完整实现模式。
同样属于门户特定内容。构建:
- 具有基于角色的访问控制的用户 CRUD API 路由
- 用于管理用户的管理员 UI 组件
用户数据模型:
interface PortalUser {
id: string
username: string
email?: string
password: string // bcrypt hash
role: 'admin' | 'viewer' | 'editor'
domoRole?: string // For edit mode: Admin | Privileged | Editor | Participant
mappingValue?: string | string[] // For edit mode: Identity Broker routing
dashboards: UserDashboard[] // Assigned embeds with per-user filters
lastLogin?: string
}
interface UserDashboard {
embedID: string
name: string
filters: DashboardFilter[]
datasetRedirects?: Record<string, string>
}
interface DashboardFilter {
column: string
operator: string
values: (string | number)[]
datasourceId?: string
}
每个用户的 dashboards 数组定义了其可见内容以及哪些过滤器会限制其数据。这会直接传入第 4 步的嵌入令牌请求。
角色层级:
- Admin:对所有用户拥有完整 CRUD 权限,管理仪表板注册表,可分配任意角色
- Editor:可创建查看者;若启用编辑模式,还可编辑 Domo 中的仪表板
- Viewer:仅可查看已分配的仪表板,无用户管理权限
每用户过滤器配置 UI
用户编辑模态框必须为每个已分配的仪表板包含一个过滤器编辑器。否则,过滤器只能通过直接编辑数据库来设置 —— 这意味着门户的核心数据隔离功能没有管理界面。
对于每一个被勾选的仪表板复选框,在其下方展开一个过滤器区域,管理员可以在其中:
- 添加过滤规则(列名、运算符下拉菜单、逗号分隔的值)
- 内联编辑现有过滤规则
- 删除单个过滤规则
- 查看所有 Domo 支持的运算符:
EQUALS、NOT_EQUALS、IN、NOT_IN、GREATER_THAN、LESS_THAN、BETWEEN、LIKE
阅读 references/auth-and-users.md 获取完整的过滤器编辑器组件实现。
第 3 步:仪表板注册表
阅读 references/auth-and-users.md 获取完整实现模式。
门户特定内容。可用仪表板(嵌入 ID)列表必须是动态的,而非硬编码。将它们与用户存储在同一数据层中(AppDB、外部 DB 等),并通过管理员 UI 进行管理。
构建:
- 仪表板 CRUD API 路由(创建/更新/删除仅限管理员,列表需身份验证)
- 用于添加新仪表板(名称 + 嵌入 ID)、重命名和删除的管理员 UI
- 从仪表板注册表动态读取的侧边栏导航
仪表板数据模型:
interface PortalDashboard {
id: string // Document ID from storage
embedID: string // Domo embed ID (5-char from embed dialog)
name: string // Display name in sidebar and admin UI
}
如果使用 AppDB,请与 Users 一起创建 Dashboards 集合。侧边栏在挂载时获取 GET /api/dashboards。管理员可看到所有仪表板;非管理员用户仅能看到已分配的仪表板(来自其 user.dashboards 数组)。
为何重要: 硬编码嵌入 ID 意味着每次添加或移除仪表板都需要修改代码并重新部署。动态注册表让管理员可以自助操作 —— 添加新的 Domo 嵌入、在门户中注册并分配给用户,全程无需改动代码。
第 4 步:只读嵌入(若为只读或两者兼具)
子技能交接 —— 必需,即使面对的是聚焦性问题也不例外。 OAuth → 嵌入令牌流程完全由
programmatic-filters负责。在此处实现会重复一个受维护的技能,并制造一个会随时间漂移的第二事实来源。该子技能包含完整实现,包括边界情况、令牌大小限制、SQL 过滤器以及数据集切换,这些都很容易被遗漏。跳过它而遵循内联版本的用户往往会遇到生产问题。你的回复必须明确点名programmatic-filters,并且只提供下面的门户接线。
在你的回复中,告诉用户:
"对于嵌入令牌流程(OAuth → 访问令牌 → 嵌入令牌 → iframe),请遵循
programmatic-filters技能。以下是连接你的用户数据与它的门户特定接线:"
然后仅提供此接线 —— 不包含嵌入令牌流程内部的任何内容:
// app/api/getembedtoken/route.ts — portal wiring only
// OAuth + embed token implementation: follow programmatic-filters
export async function POST(req: NextRequest) {
// 1. Verify portal session (Step 1)
const token = req.cookies.get('token')?.value
const auth = await verifyToken(token!)
if (auth.status !== 200) return NextResponse.json({ message: 'Unauthorized' }, { status: auth.status })
const user = auth.data!.user
const { embedID } = await req.json()
// 2. Portal authorization: user must have this dashboard assigned (Step 2 data model)
// Admins can access any dashboard in the registry (Step 3)
const dashboard = user.dashboards?.find((d: any) => d.embedID === embedID)
if (!dashboard && user.role !== 'admin') {
return NextResponse.json({ message: 'Dashboard not assigned' }, { status: 403 })
}
// 3. Hand off to programmatic-filters with these portal values:
// embedID → authorization[].token
// dashboard.filters → authorization[].filters
// dashboard.datasetRedirects → authorization[].datasetRedirects
// programmatic-filters handles the OAuth call, embed token request, and response format.
}
步骤 5:编辑嵌入(如果选择 edit 或 both)
子技能交接 — 必需,即使用户专门询问的是编辑嵌入。 Identity Broker JWT 流程完全由
edit-embed负责。这一点没有商量余地:不要写jwt.sign(),不要构造 IDP URL,不要内联编写 iframe 组件。以下是为什么这很重要 — 该子技能涵盖了重放保护(jti 唯一性)、多实例路由的边界情况、Domo 的 HS256 要求,以及 5 分钟 JWT 过期后的会话持久化行为。内联实现至少会遗漏其中一部分。用户需要遵循该技能才能获得生产安全的结果。你在这里的价值是提供门户侧的接线,将他们的用户数据连接到该流程 — 而不是重新实现它。
在你的回复中,告诉用户:
"对于 Identity Broker JWT 流程(JWT 创建、IDP URL、iframe 渲染),请遵循
edit-embed技能。以下是将你的用户数据连接到它的门户专属接线:"
然后只提供这些接线 — 不包含 JWT 或 iframe 流程内部的任何内容:
// app/api/editembed/route.ts — portal wiring only
// Identity Broker JWT implementation: follow edit-embed
export async function POST(req: NextRequest) {
// 1. Verify portal session (Step 1)
const token = req.cookies.get('token')?.value
const auth = await verifyToken(token!)
if (auth.status !== 200) return NextResponse.json({ message: 'Unauthorized' }, { status: auth.status })
const user = auth.data!.user
// 2. Hand off to edit-embed with these portal values:
// user.username / user.email → sub, name, email JWT fields
// user.domoRole → role field (Admin|Privileged|Editor|Participant)
// user.mappingValue → KEY_ATTRIBUTE field (split comma-separated → array)
// edit-embed handles jwt.sign(), IDP URL construction, and iframe rendering.
// Sign with JWT_SECRET (Domo's secret) — NOT APP_JWT_SECRET (your login sessions secret).
}
这一步还有一个需要向用户指出的关键区别:两个密钥,两种用途 — APP_JWT_SECRET 用于签发你的门户登录会话(步骤 1),JWT_SECRET 用于签发 Domo Identity Broker 令牌(本步骤)。它们绝不能互换。
步骤 6:客户端交互性(可选)
子技能交接 — 必需。 MessagePort 设置、筛选器 API 和事件处理完全由
jsapi-filters负责。在这里实现它们会与该技能重复。你的工作只是门户挂载模式。
在你的回复中,告诉用户:
"对于 JS API(MessagePort 设置、筛选器应用、钻取事件、iframe 尺寸调整),请遵循
jsapi-filters技能。以下是如何在门户中挂载它:"
然后只提供挂载模式:
// app/layout.tsx — mount before any Domo iframe loads
// Full JS API implementation: follow jsapi-filters
import DomoJsApiInitializer from './components/DomoJsApiInitializer'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<DomoJsApiInitializer /> {/* Must be in root layout, before iframes render */}
{children}
</body>
</html>
)
}
需要提及的常见门户模式(jsapi-filters 实现了所有这些):
- 嵌入上方的筛选栏 → 来自该子技能的
applyFilters() - 在一个嵌入中钻取 → 通过
applyFiltersToEmbed(referenceId, ...)筛选另一个嵌入 - iframe 自动调整尺寸 → 该子技能中的
onFrameSizeChange事件
步骤 7:应用品牌标识
在功能部分就位后,应用在第 1 阶段收集的品牌指南:
Tailwind 配置:
export default {
theme: {
extend: {
colors: {
primary: '{user_primary}',
secondary: '{user_secondary}',
accent: '{user_accent}',
},
fontFamily: {
sans: ['{user_font}', ...defaultTheme.fontFamily.sans],
},
},
},
}
将品牌颜色应用到页眉、侧边栏、按钮、表单和登录页面。在页眉和登录页面中使用 logo。仪表板是主要内容 — 门户外观应为其提供框架,而不是与之竞争。
组合只读模式和编辑模式
如果用户选择了 "both" 模式,告诉他们构建一个包含两个分支的单一 EmbedDashboard 组件:
- 只读分支:遵循
programmatic-filters(POST 表单 → iframe) - 编辑分支:遵循
edit-embed(直接 iframesrc→ Identity Broker URL)
唯一的门户专属接线是根据仪表板分配检测使用哪种模式:
// EmbedDashboard.tsx — mode detection only; each branch follows its sub-skill
const isEditMode = dashboard.isEditable // flag on the UserDashboard object
const endpoint = isEditMode ? '/api/editembed' : '/api/getembedtoken'
// Read-only rendering: follow programmatic-filters
// Edit rendering: follow edit-embed
密码重置
如果使用 AppDB 或外部数据库(而非第三方认证提供商),请实现密码重置。阅读 references/auth-and-users.md 了解实现模式,包括速率限制。
安全检查清单
生产前:
- 所有密钥都在环境变量中,绝不在客户端代码中
- 密码使用 bcrypt 哈希(10+ 轮盐)
- 会话令牌放在 HttpOnly cookies 中(而非 localStorage)
- 嵌入令牌仅在服务端生成
- 管理路由由中间件保护
- 编程式筛选器在服务端强制执行(客户端筛选器仅用于 UX)
- 编辑 JWT 令牌短期有效(5 分钟)
- 登录和密码重置端点有速率限制
部署
常见选项:
- Vercel / Netlify — 对 Next.js 最简单
- Docker — 用于本地部署或云 VM 的 Node.js Alpine 镜像
- Domo Code Engine — 将所有内容保留在 Domo 中(查看当前限制)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
ENV NODE_ENV=production
CMD ["npm", "start"]