mcpField
E
edit-embed

edit-embed

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

关于 edit-embed

edit-embed 是用于嵌入可编辑 Domo 内容的开发技能,介绍 Identity Broker 的 JWT 流程、角色权限、实例路由和 iframe 设置。

主要功能

  1. 01创建 Identity Broker 认证所需的 JWT
  2. 02按属性将用户路由到 Domo 实例
  3. 03支持分配 Domo 角色
  4. 04通过 iframe 嵌入编辑会话
  5. 05提供指向 Domo 页面的深层链接

适用场景

  1. 01允许外部用户编辑嵌入式仪表板
  2. 02将租户用户路由到对应的 Domo 实例
  3. 03按角色授予用户编辑权限

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/stahura/domo-ai-vibe-rules --skill edit-embed

安装后在 Claude Code 中输入/edit-embed确认已加载。

附带文件references/identity-broker.md

详细介绍

Domo 嵌入式编辑体验

让外部用户通过嵌入式 iframe 创建、编辑、保存和共享 Domo 内容(仪表板、卡片、警报、报告、数据源)。使用带 JWT 认证的 Domo Identity Broker——与只读嵌入有本质区别。

关于只读嵌入,请参阅 programmatic-filters。关于客户端筛选,请参阅 jsapi-filters。

工作原理

  1. 你的服务器对用户进行身份验证,并创建一个包含身份、角色和路由属性的 JWT
  2. JWT 使用来自 Domo 的共享密钥进行签名
  3. 服务器构造编辑 URL:{IDP_URL}/jwt?token={jwt_token}
  4. 客户端将 URL 直接渲染为 iframe 的 src(无需 POST 表单)
  5. Identity Broker 验证 JWT,并根据角色将用户路由到正确的 Domo 实例

关键区别:只读 = OAuth → 嵌入令牌 → POST 表单。编辑 = JWT → Identity Broker URL → iframe src。

先决条件

与你的 Domo CSM 合作设置 Identity Broker。提供:你的 Domo 实例 URL、认证方式(JWT)、路由属性键,以及属性到实例的映射。

你将获得:Identity Broker URL、JWT 签名密钥(UUID),以及属性键配置。

必需的环境变量: IDP_URL(Broker URL)、JWT_SECRET(签名密钥)、KEY_ATTRIBUTE(路由属性名称)。


Domo Identity Broker

对用户进行身份验证(验证 JWT 签名),并根据映射属性将其路由到正确的 Domo 实例。支持 SAML2、OIDC、JWT、OAuth2——JWT 是嵌入场景中最常用的方式。

实例映射

每个映射属性值对应一个 Domo 实例:

属性值Domo 实例
acme-corpacme.domo.com
globexglobex.domo.com
initechinitech.domo.com

通过你的 CSM 配置(webform 数据集或 Excel)。以逗号分隔的值可路由到多个实例。


JWT 令牌结构

必需字段
字段类型描述
substring用户的标识符——通常是其电子邮件地址
expnumber过期时间(EPOCH 时间戳)。请保持较短(建议 5 分钟)
jtistring唯一的令牌标识符。使用 UUID v4 以防止重放攻击
{KEY_ATTRIBUTE}string 或 string[]将用户映射到 Domo 实例的路由属性
常见可选字段
字段类型描述
namestring用户在 Domo 中的显示名称
emailstring用户的电子邮件地址
rolestringDomo 角色:Admin、Privileged、Editor 或 Participant
employee_idstring员工标识符
titlestring职位头衔
departmentstring部门名称
locationstring位置
phonestring电话号码
localestring区域设置偏好
timezonestring时区偏好
groupsstring[]Domo 内的组分配
Domo 角色
角色能力
Admin完全访问权限 — 管理用户、数据、内容和设置
Privileged创建/编辑仪表板、卡片、数据流;管理数据源
Editor创建/编辑仪表板和卡片;数据源访问权限受限
Participant仅查看和交互共享内容(省略时的默认值)

大多数外部用户应为 Editor 或 Participant。


服务端实现

第 1 步:创建 JWT Token

Node.js / TypeScript:

import jwt from "jsonwebtoken";
import { v4 as uuidv4 } from "uuid";

function createEditToken(user: {
  username: string;
  email: string;
  domoRole?: string;
  mappingValue?: string | string[];
}) {
  const jwtBody: Record<string, unknown> = {
    sub: user.username,
    name: user.username,
    email: user.email,
    role: user.domoRole || "Participant",
    jti: uuidv4(),
  };

  // Add the routing attribute for instance mapping
  const keyAttribute = process.env.KEY_ATTRIBUTE;
  if (keyAttribute && user.mappingValue) {
    jwtBody[keyAttribute] = user.mappingValue;
  }

  return jwt.sign(jwtBody, process.env.JWT_SECRET!, {
    expiresIn: "5m",
    algorithm: "HS256",
  });
}

Python:

import jwt
import uuid
import time
import os

def create_edit_token(user):
    payload = {
        'sub': user['username'],
        'name': user['username'],
        'email': user['email'],
        'role': user.get('domo_role', 'Participant'),
        'jti': str(uuid.uuid4()),
        'exp': int(time.time()) + 300  # 5 minutes
    }

    key_attribute = os.environ.get('KEY_ATTRIBUTE')
    if key_attribute and user.get('mapping_value'):
        payload[key_attribute] = user['mapping_value']

    return jwt.encode(payload, os.environ['JWT_SECRET'], algorithm='HS256')
第 2 步:构建编辑 URL
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;

可选:深层链接到特定页面:

const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}&destination=/page/${pageId}`;
第 3 步:构建 API 路由

Next.js App Router:

// app/api/editembed/route.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
import { v4 as uuidv4 } from "uuid";

export async function POST(req: NextRequest) {
  try {
    const { embedID } = await req.json();

    // Authenticate the user against your own auth system
    const token = req.cookies.get("token")?.value;
    if (!token) {
      return NextResponse.json(
        { message: "Unauthorized: Please log in" },
        { status: 401 },
      );
    }

    const user = await verifyAndGetUser(token);
    if (!user) {
      return NextResponse.json({ message: "User not found" }, { status: 404 });
    }

    // Handle comma-separated mapping values
    let mappingValue = user.mappingValue;
    if (typeof mappingValue === "string" && mappingValue.includes(",")) {
      mappingValue = mappingValue.split(",").map((s: string) => s.trim());
    }

    // Build the JWT payload
    const jwtBody: Record<string, unknown> = {
      sub: user.username,
      name: user.username,
      role: user.domoRole || "Participant",
      email: user.email,
      jti: uuidv4(),
    };

    const keyAttr = process.env.KEY_ATTRIBUTE;
    if (keyAttr && mappingValue) {
      jwtBody[keyAttr] = mappingValue;
    }

    // Sign the token
    const editToken = jwt.sign(jwtBody, process.env.JWT_SECRET || "", {
      expiresIn: "5m",
    });

    // Return the Identity Broker URL
    const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;
    return NextResponse.json(editUrl);
  } catch (error) {
    console.error("Error in /api/editembed:", error);
    return NextResponse.json(
      { message: "Server error occurred" },
      { status: 500 },
    );
  }
}

Express 示例:

app.post("/api/editembed", authenticateUser, (req, res) => {
  const user = req.user;

  let mappingValue = user.mappingValue;
  if (typeof mappingValue === "string" && mappingValue.includes(",")) {
    mappingValue = mappingValue.split(",").map((s) => s.trim());
  }

  const jwtBody = {
    sub: user.username,
    name: user.username,
    role: user.domoRole || "Participant",
    email: user.email,
    jti: uuidv4(),
  };

  const keyAttr = process.env.KEY_ATTRIBUTE;
  if (keyAttr && mappingValue) {
    jwtBody[keyAttr] = mappingValue;
  }

  const editToken = jwt.sign(jwtBody, process.env.JWT_SECRET, {
    expiresIn: "5m",
  });

  const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;
  res.json(editUrl);
});
第 4 步:在 Iframe 中渲染

编辑嵌入通过 iframe src 直接加载(不像只读嵌入那样需要 POST 表单):

function EditEmbed({ editUrl }: { editUrl: string }) {
  return (
    <iframe
      src={editUrl}
      style={{ width: "100%", height: "100%", border: "none" }}
      allow="fullscreen"
    />
  );
}

处理只读模式与编辑模式

如果你的应用同时支持只读模式和编辑模式,你的嵌入组件需要处理这两种流程:

function EmbedDashboard({ embedID }: { embedID: string }) {
  const [embedURL, setEmbedURL] = useState<string | null>(null);
  const [embedToken, setEmbedToken] = useState<string | null>(null);
  const isEditMode = embedID === "edit";

  useEffect(() => {
    const endpoint = isEditMode ? "/api/editembed" : "/api/getembedtoken";

    fetch(endpoint, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ embedID }),
      credentials: "include",
    })
      .then((res) => res.json())
      .then((data) => {
        if (isEditMode) {
          setEmbedURL(data); // Edit returns a direct URL string
          setEmbedToken(null);
        } else {
          setEmbedURL(data.embedUrl); // Read-only returns { embedUrl, embedToken }
          setEmbedToken(data.embedToken);
        }
      });
  }, [embedID, isEditMode]);

  // Edit mode: direct iframe src
  if (isEditMode && embedURL) {
    return (
      <iframe
        src={embedURL}
        style={{ width: "100%", height: "100%", border: "none" }}
      />
    );
  }

  // Read-only mode: POST form submission to iframe
  // (handle embedToken + form submission as in programmatic-filters skill)
}

编辑嵌入的用户管理

关键用户属性
属性用途示例
domoRole控制编辑能力'Editor'、'Participant'
mappingValue将用户路由到正确的 Domo 实例'acme-corp' 或 ['acme-corp', 'globex']
emailDomo 用户身份所必需'[email protected]'
多实例用户

如果用户需要访问多个 Domo 实例,将其 mappingValue 存储为逗号分隔的字符串或数组:

// Single instance
user.mappingValue = "acme-corp";

// Multiple instances
user.mappingValue = "acme-corp, globex";
// or
user.mappingValue = ["acme-corp", "globex"];

构建 JWT 时,处理这两种格式:

let mappingValue = user.mappingValue;
if (typeof mappingValue === "string" && mappingValue.includes(",")) {
  mappingValue = mappingValue.split(",").map((s) => s.trim());
}

深层链接

const editUrl = `${IDP_URL}/jwt?token=${editToken}&destination=/page/${pageId}`;
const editUrl = `${IDP_URL}/jwt?token=${editToken}&destination=/kpicard/${cardId}`;

注意事项与最佳实践

  • Token 过期: 保持 JWT 短期有效(推荐 5 分钟)。仅用于初始认证 — 会话在此之后持续存在。
  • 签名算法: 使用 HS256。Domo 的 Identity Broker 要求如此。
  • JTI 唯一性: 始终使用 UUID v4。Domo 可能会拒绝重复使用的 JTI 值(重放保护)。
  • 映射值: KEY_ATTRIBUTE 名称是你与 CSM 商定的任何名称(customer、keyAttribute、tenant)。值必须与 Domo 的实例映射完全匹配。
  • 密钥管理: 绝不要在客户端暴露 JWT_SECRET。使用环境变量或密钥管理器。
  • 安全模型: 只读嵌入限定于特定仪表板并带有过滤器。编辑嵌入为每个角色提供完整的 Domo 会话 — 分配角色时要保守。
  • 实例映射变更: 使用稳定的值(租户 ID,而非可能变更的公司名称)。

环境变量参考

变量描述示例
IDP_URLIdentity Broker URLhttps://yourcompany.identity.domo.com
JWT_SECRET共享签名密钥(UUID 格式)aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee
KEY_ATTRIBUTE用于实例路由的属性键keyAttribute、customer、tenant

TypeScript 类型定义

type DomoRole = "Admin" | "Privileged" | "Editor" | "Participant";

interface EditEmbedUser {
  username: string;
  email: string;
  domoRole?: DomoRole;
  mappingValue?: string | string[];
}

interface EditJwtPayload {
  sub: string;
  name?: string;
  email?: string;
  role?: DomoRole;
  jti: string;
  exp?: number;
  [keyAttribute: string]: unknown; // dynamic routing attribute
}

快速参考

阅读 references/identity-broker.md 以获取有关 Identity Broker 配置和实例映射管理的更多详细信息。

评论

支持 Markdown