关于 edit-embed
edit-embed 是用于嵌入可编辑 Domo 内容的开发技能,介绍 Identity Broker 的 JWT 流程、角色权限、实例路由和 iframe 设置。
主要功能
- 01创建 Identity Broker 认证所需的 JWT
- 02按属性将用户路由到 Domo 实例
- 03支持分配 Domo 角色
- 04通过 iframe 嵌入编辑会话
- 05提供指向 Domo 页面的深层链接
适用场景
- 01允许外部用户编辑嵌入式仪表板
- 02将租户用户路由到对应的 Domo 实例
- 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。
工作原理
- 你的服务器对用户进行身份验证,并创建一个包含身份、角色和路由属性的 JWT
- JWT 使用来自 Domo 的共享密钥进行签名
- 服务器构造编辑 URL:
{IDP_URL}/jwt?token={jwt_token} - 客户端将 URL 直接渲染为 iframe 的
src(无需 POST 表单) - 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-corp | acme.domo.com |
globex | globex.domo.com |
initech | initech.domo.com |
通过你的 CSM 配置(webform 数据集或 Excel)。以逗号分隔的值可路由到多个实例。
JWT 令牌结构
必需字段
| 字段 | 类型 | 描述 |
|---|---|---|
sub | string | 用户的标识符——通常是其电子邮件地址 |
exp | number | 过期时间(EPOCH 时间戳)。请保持较短(建议 5 分钟) |
jti | string | 唯一的令牌标识符。使用 UUID v4 以防止重放攻击 |
{KEY_ATTRIBUTE} | string 或 string[] | 将用户映射到 Domo 实例的路由属性 |
常见可选字段
| 字段 | 类型 | 描述 |
|---|---|---|
name | string | 用户在 Domo 中的显示名称 |
email | string | 用户的电子邮件地址 |
role | string | Domo 角色:Admin、Privileged、Editor 或 Participant |
employee_id | string | 员工标识符 |
title | string | 职位头衔 |
department | string | 部门名称 |
location | string | 位置 |
phone | string | 电话号码 |
locale | string | 区域设置偏好 |
timezone | string | 时区偏好 |
groups | string[] | 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'] |
email | Domo 用户身份所必需 | '[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_URL | Identity Broker URL | https://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 配置和实例映射管理的更多详细信息。