چگونه با استفاده از Metabase SDK و FastAPI، دسترسی امن و محدود به داده‌های داشبوردها ایجاد کردیم؟

در تیم پشتیبانی ، نیاز داشتیم اپراتورها فقط به داده‌هایی خاص از داشبوردهای متابیس دسترسی داشته باشن — نه بیشتر.

ما نمی‌خواستیم:

  • کاربر وارد محیط متابیس بشه،

  • چیزی از کوئری‌ها ببینه،

  • یا حتی به لینک یا iframe داشبوردها دسترسی داشته باشه.


🎯 راه‌حل نهایی: React + Metabase SDK + FastAPI Proxy

در نهایت، این معماری رو پیاده‌سازی کردیم:

  • در سمت فرانت‌اند، از Metabase Embedding SDK استفاده کردیم.

  • این SDK امکان رندر مستقیم داشبورد داخل کامپوننت‌های ری‌اکت رو می‌ده.

  • اما برای امنیت بیشتر، جلوی ارتباط مستقیم SDK با متابیس رو گرفتیم و یک لایه پروکسی با FastAPI بینشون قرار دادیم.


جریان کار به این شکله:

  1. کاربر وارد پنل پشتیبانی می‌شه (احراز هویت با JWT داخلی ما).

  2. کاربر تنها داشبوردهای رو می‌بینه که با توجه به رول/پرمیشن‌هاش در سیستم تعریف شدن.

  3. وقتی SDK می‌خواد داده‌ی یک داشبورد رو بگیره، درخواست رو به جای متابیس، به سرویس پروکسی ما می‌فرسته.

  4. در پروکسی (FastAPI):

    • JWT اعتبارسنجی می‌شه.

    • پرمیشن کاربر برای این داشبورد چک می‌شه.

    • در صورت تایید، درخواست به API متابیس فوروارد می‌شه.

  5. پاسخ متابیس به‌صورت JSON دریافت می‌شه و به SDK برگشت داده می‌شه.

  6. SDK داده رو گرفته و خودش داخل ری‌اکت رندر می‌کنه .


چرا SDK رو به‌جای embed URL انتخاب کردیم؟

  • کنترل کامل روی ظاهر و تعامل در ری‌اکت

  • بدون استفاده از iframe و محدودیت‌هایش

  • امکان شخصی‌سازی request ها و header ها

  • خروجی JSON آماده برای ویژوال‌سازی دلخواه


نکات امنیتی مهم

  • ارتباط مستقیم بین کاربر و متابیس کاملاً قطع شده.

  • فقط پروکسی ما به API متابیس دسترسی داره.

  • سطح دسترسی هر کاربر قبل از ارسال request به متابیس، در سرویس ما بررسی می‌شه.

  • داده‌ی خام، کوئری‌ها یا اطلاعات متا به کاربر داده نمی‌شه؛ فقط خروجی پاک‌شده و کنترل‌شده از متابیس.


نمونه کد سمپل پروکسی سرویس :

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse, Response
import httpx
import json
from urllib.parse import urlencode

app = FastAPI()

METABASE_URL = "https://your-metabase-url.com"  # ← آدرس متابیس
METABASE_API_KEY = "your-api-key"               # ← کلید API متابیس


@app.api_route("/{path:path}", methods=["GET", "POST", "OPTIONS"])
async def proxy_metabase(path: str, request: Request):
    # ⛔️ اینجا می‌تونی دسترسی‌های کاربر رو بررسی کنی
    # --------------------------------------------------------------------
    # مثال:
    # user = await get_current_user(request)
    # if not user.has_permission(path):
    #     raise HTTPException(status_code=403, detail="Permission denied")
    # --------------------------------------------------------------------

    # ساخت آدرس کامل متابیس
    query_string = urlencode(dict(request.query_params))
    full_url = f"{METABASE_URL}/{path}"
    if query_string:
        full_url += f"?{query_string}"

    # گرفتن بدنه درخواست در صورت نیاز
    body = None
    if request.method in {"POST", "PUT", "PATCH"}:
        raw = await request.body()
        if raw:
            body = json.loads(raw.decode("utf-8"))

    # ارسال درخواست به متابیس
    async with httpx.AsyncClient() as client:
        response = await client.request(
            method=request.method,
            url=full_url,
            json=body,
            headers={
                "x-api-key": METABASE_API_KEY,
                "User-Agent": "fastapi-proxy"
            }
        )

    # بازگرداندن پاسخ
    content_type = response.headers.get("content-type", "")
    if "application/json" in content_type:
        return JSONResponse(content=response.json(), status_code=response.status_code)
    return Response(
        content=response.content,
        status_code=response.status_code,
        media_type=content_type
    )

سمت فرانت هم به این شکل عمل کردیم:

📁 ساختار پیشنهادی

src/
├── metabase/
│   ├── MetabaseProvider.jsx
│   ├── Dashboard.jsx
│   └── config.js

✅ config.js (تنظیمات اتصال به متابیس)

import { defineMetabaseAuthConfig, defineMetabaseTheme } from '@metabase/embedding-sdk-react'

const config = defineMetabaseAuthConfig({
  preferredAuthMethod: 'jwt',
  metabaseInstanceUrl: 'https://proxy-api.example.com', // آدرس متابیس پروکسی شما

  // درخواست گرفتن توکن JWT از API بک‌اند
  fetchRequestToken: async () => {
    const response = await fetch('https://proxy-api.example.com/auth/sso?preferred_method=jwt', {
      method: 'GET',
      credentials: 'include', // ← برای ارسال کوکی‌ها
    })

    return response.json() // باید { token: '...' } برگردونه
  },
})

// تم اختیاری (می‌تونید حذفش کنید)
const theme = defineMetabaseTheme({ colorScheme: 'light' })

export { config, theme }

✅ MetabaseProvider.jsx (Provider اصلی برای کانتکست)

import { config, theme } from './config'
import { MetabaseProvider as SDKProvider } from '@metabase/embedding-sdk-react'

function MetabaseProvider({ children }) {
  return (
    <SDKProvider authConfig={config} theme={theme}>
      {children}
    </SDKProvider>
  )
}

export { MetabaseProvider }

✅ Dashboard.jsx (نمایش داشبورد خاص با پارامتر)

import { useMemo } from 'react'
import { useLocation, useParams } from 'react-router-dom'
import { StaticDashboard, MetabaseProvider } from '@metabase/embedding-sdk-react'

function objectifyQuery(search) {
  const query = new URLSearchParams(search)
  const obj = {}
  for (const [key, value] of query.entries()) {
    obj[key] = value
  }
  return obj
}

function DashboardPage() {
  const { id } = useParams() // مثلاً /dashboard/12
  const { search } = useLocation()
  const queryParams = useMemo(() => objectifyQuery(search), [search])

  return (
    <div style={{ height: '100vh' }}>
      <MetabaseProvider>
        <StaticDashboard
          dashboardId={+id}
          initialParameters={queryParams}
          withTitle
        />
      </MetabaseProvider>
    </div>
  )
}

export default DashboardPage

نتیجه‌گیری

اگر به دنبال پیاده‌سازی یک سیستم امن و حرفه‌ای برای نمایش داشبوردهای متابیس هستید که:

  • دسترسی‌ها و پرمیشن‌ها را به صورت دقیق و مبتنی بر نقش کاربران مدیریت کند،

  • بدون استفاده از iframe و با حفظ یکپارچگی تجربه کاربری در فرانت‌اند ری‌اکت باشد،

  • کوئری‌ها و لینک‌های حساس متابیس را از دید کاربران نهایی مخفی نگه دارد،

  • و قابلیت توسعه و سفارشی‌سازی آسان را داشته باشد،

ترکیب استفاده از بسته @metabase/embedding-sdk همراه با یک سرویس پروکسی امن مبتنی بر FastAPI، راهکاری ایده‌آل، مقیاس‌پذیر و قابل اعتماد خواهد بود که به راحتی می‌توانید آن را مطابق نیازهای سازمان خود توسعه دهید و بهینه‌سازی کنید.

نمونه داشبورد پیاده سازی شده :