mcpField
E
embed-portal

embed-portal

github.com/stahura开发24190 次安装26 周前更新
代码仓库

关于 embed-portal

embed-portal 是用于规划和构建 Domo 嵌入门户的技能,涵盖需求收集、认证、用户与仪表板管理,并将嵌入流程交由专门子技能处理。

主要功能

  1. 01梳理品牌和嵌入模式需求
  2. 02规划认证与用户管理
  3. 03定义动态仪表板目录
  4. 04委托只读和编辑嵌入流程
  5. 05衔接可选的客户端交互

适用场景

  1. 01规划带品牌样式的 Domo 仪表板门户
  2. 02管理门户用户及仪表板分配
  3. 03组合只读和可编辑嵌入
  4. 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(直接 iframe src → 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"]

评论

支持 Markdown