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:要件を収集する
コードを書く前に、3 つの情報を集めます。会話形式で質問し、すべての質問を一度に並べないでください。回答がすでに文脈から明らかな場合は、次の手順に進みます。
1. ブランドガイドライン
次のように質問します。「ポータルにはどのようなブランディングを使用しますか?カラーパレット(プライマリ、セカンダリ、アクセント)、フォント、ロゴがあれば教えてください。」
強いこだわりがない場合の妥当なデフォルト:
- プライマリ:
#1A1A2E、セカンダリ:#16213E、アクセント:#0F3460、暗い背景上のテキスト:#E0E0E0 - フォント:Inter(またはシステムフォントスタック)
- ロゴ:アプリ名を使ったプレースホルダー
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) — 最もすばやくログインを設定でき、パスワードのリセットや MFA に標準で対応します。
デフォルトの推奨:Domo AppDB — すべてを Domo エコシステム内に保てます。
フェーズ 2:アプリケーションのひな形を作成する
Next.js(App Router、TypeScript、Tailwind CSS)をおすすめします。API レイヤーと UI の両方を 1 つのプロジェクトで扱えるため、埋め込みトークンのフローを安全に保てます。ユーザーが別のフレームワークを希望する場合は、適宜対応してください。基本的なパターンはフレームワークに依存しません。
プロジェクト構成
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 時間とし、Cookie には HttpOnly、Secure、SameSite=Lax を設定します。
- このアプリでは異なる JWT を 2 つ使います。1 つはログインセッション用、もう 1 つは 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 の埋め込みトークンリクエストに直接渡されます。
ロールの階層:
- 管理者:すべてのユーザーに対する完全な CRUD 権限、ダッシュボードレジストリの管理、任意のロールの割り当てが可能
- 編集者:閲覧者を作成できます。編集モードが有効な場合は、Domo でダッシュボードを編集することもできます
- 閲覧者:割り当てられたダッシュボードのみを表示でき、ユーザー管理はできません
ユーザーごとのフィルター設定 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が担います。ここで実装すると、保守されているスキルと重複し、時間の経過とともに乖離する第 2 の情報源を作ることになります。このサブスキルには、見落としやすいエッジケース、トークンサイズの上限、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:埋め込みの編集(編集モードまたは両方の場合)
サブスキルへの引き継ぎ — 編集用埋め込みについて具体的に質問されている場合でも必須です。 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).
}
このステップでは、ユーザーに注意を促すべき重要な違いもあります。2 つのシークレットには、それぞれ異なる用途があります。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],
},
},
},
}
ヘッダー、サイドバー、ボタン、フォーム、ログインページにブランドカラーを適用します。ヘッダーとログインページにはロゴを使用します。ダッシュボードがメインコンテンツです。ポータルの外枠はダッシュボードを引き立てるようにし、競合しないようにします。
読み取り専用モードと編集モードの組み合わせ
ユーザーが「両方」のモードを選択した場合、2 つの分岐を持つ単一の 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 または外部 DB(サードパーティの認証プロバイダーではないもの)を使用する場合は、パスワードのリセットを実装してください。レート制限を含む実装パターンについては、references/auth-and-users.md を参照してください。
セキュリティチェックリスト
本番環境に移行する前に、次の項目を確認してください。
- すべてのシークレットを環境変数に保存し、クライアント側のコードには含めない
- パスワードは bcrypt でハッシュ化する(ソルトラウンド数は 10 以上)
- セッショントークンは HttpOnly Cookie に保存する(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"]