Overview
Your API should expose endpoints for:- Creating dashboards for tenants
- Retrieving tenant dashboards
- Generating iframe embed tokens
- Managing dashboard access
Authentication Middleware
All endpoints should require authentication. Here’s an example middleware:from fastapi import Depends, HTTPException, Header
from jose import jwt, JWTError
async def get_authenticated_user(
authorization: str = Header(None)
) -> dict:
"""Verify JWT token and return user info"""
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing authorization")
token = authorization.replace("Bearer ", "")
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
import jwt from "jsonwebtoken";
function authenticateUser(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing authorization" });
}
const token = authHeader.replace("Bearer ", "");
try {
const payload = jwt.verify(token, process.env.SECRET_KEY);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: "Invalid token" });
}
}
Create Dashboard Endpoint
The
create_dashboard function being called in the endpoint below should have already been created in
the Creating Dashboards guide.from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/api/v1/dashboards", tags=["dashboards"])
class DashboardCreate(BaseModel):
tenant_id: str
workspace_id: str
name: str
description: Optional[str] = None
@router.post("/")
async def create_dashboard(
dashboard: DashboardCreate,
current_user: Dict = Depends(get_authenticated_user),
db: Session = Depends(get_db)
):
"""Create a dashboard for a tenant"""
# Verify user has permission to create dashboard for this tenant
if not has_access(current_user, dashboard.tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
handler = DashboardHandler(db)
result = await handler.create_dashboard(
tenant_id=dashboard.tenant_id,
workspace_id=dashboard.workspace_id,
name=dashboard.name,
description=dashboard.description,
created_by=current_user["user_id"]
)
return {
"success": True,
"data": result
}
import express from "express";
const router = express.Router();
router.post("/dashboards", authenticateUser, async (req, res) => {
const { tenantId, workspaceId, name, description } = req.body;
// Verify user has permission
if (!hasAccess(req.user, tenantId)) {
return res.status(403).json({ error: "Access denied" });
}
try {
const dashboard = await createDashboard(
tenantId,
workspaceId,
name,
description
);
res.json({
success: true,
data: dashboard,
});
} catch (error) {
res.status(500).json({
error: error.message,
});
}
});
<?php
Route::post('/dashboards', function (Request $request) {
$validated = $request->validate([
'tenant_id' => 'required|string',
'workspace_id' => 'required|string',
'name' => 'required|string',
'description' => 'nullable|string',
]);
// Verify user has permission
if (!hasAccess(Auth::user(), $validated['tenant_id'])) {
return response()->json(['error' => 'Access denied'], 403);
}
$dashboard = createDashboardForTenant(
$validated['tenant_id'],
$validated['workspace_id'],
$validated['name'],
$validated['description'] ?? null
);
return response()->json([
'success' => true,
'data' => $dashboard
]);
})->middleware('auth:api');
?>
POST /api/v1/dashboards
Authorization: Bearer YOUR_JWT_TOKEN
{
"tenant_id": "org-123",
"workspace_id": "workspace-456",
"name": "Sales Dashboard",
"description": "Q4 Sales Analytics"
}
{
"success": true,
"data": {
"id": "dashboard-789",
"tenant_id": "org-123",
"workspace_id": "workspace-456",
"dashboard_id": "dashboard-789",
"created_at": "2024-01-15T10:30:00Z"
}
}
Get Dashboard by Tenant
Retrieve the dashboard for a specific tenant.@router.get("/{tenant_id}")
def get_dashboard_by_tenant(
tenant_id: str,
current_user: Dict = Depends(get_authenticated_user),
db: Session = Depends(get_db)
):
"""Get dashboard for a specific tenant"""
# Verify access
if not has_access(current_user, tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
handler = DashboardHandler(db)
dashboard = handler.get_dashboard_by_tenant(tenant_id)
if not dashboard:
raise HTTPException(status_code=404, detail="Dashboard not found")
return {
"success": True,
"data": dashboard
}
router.get("/dashboards/:tenantId", authenticateUser, async (req, res) => {
const { tenantId } = req.params;
// Verify access
if (!hasAccess(req.user, tenantId)) {
return res.status(403).json({ error: "Access denied" });
}
const dashboard = await db.tenantDashboards.findOne({
where: { tenant_id: tenantId },
});
if (!dashboard) {
return res.status(404).json({ error: "Dashboard not found" });
}
res.json({
success: true,
data: dashboard,
});
});
GET /api/v1/dashboards/org-123
Authorization: Bearer YOUR_JWT_TOKEN
{
"success": true,
"data": {
"id": "dashboard-789",
"tenant_id": "org-123",
"workspace_id": "workspace-456",
"dashboard_id": "dashboard-789",
"created_at": "2024-01-15T10:30:00Z"
}
}
Generate Iframe Token
Generate a secure token for embedding a dashboard.class DashboardIframeTokenRequest(BaseModel):
tenant_id: str
workspace_id: str
dashboard_id: str
valid_until: Optional[int] = None
@router.post("/iframe-token")
def create_iframe_token(
request: DashboardIframeTokenRequest,
current_user: Dict = Depends(get_authenticated_user),
db: Session = Depends(get_db)
):
"""Generate secure token for embedding dashboard in iframe"""
# Verify tenant belongs to user
if not has_access(current_user, request.tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
handler = DashboardHandler(db)
token_data = handler.generate_iframe_token(
tenant_id=request.tenant_id,
workspace_id=request.workspace_id,
dashboard_id=request.dashboard_id,
valid_until=request.valid_until
)
return {
"success": True,
"data": token_data
}
router.post("/dashboards/iframe-token", authenticateUser, async (req, res) => {
const { tenantId, workspaceId, dashboardId, validUntil } = req.body;
// Verify tenant belongs to user
if (!hasAccess(req.user, tenantId)) {
return res.status(403).json({ error: "Access denied" });
}
const tokenData = generateIframeToken(
tenantId,
workspaceId,
dashboardId,
validUntil
);
res.json({
success: true,
data: tokenData,
});
});
POST /api/v1/dashboards/iframe-token
Authorization: Bearer YOUR_JWT_TOKEN
{
"tenant_id": "org-123",
"workspace_id": "workspace-456",
"dashboard_id": "dashboard-789",
"valid_until": 1700000000 // Optional: Unix timestamp
}
{
"success": true,
"data": {
"token": "eyJhcGlfa2V5X2lkIjoieW91ci1hcGkta2V5IiwidGVuYW50X2lkIjoib3JnLTEyMyIsImRhc2hib2FyZF9pZCI6ImRhc2hib2FyZC03ODkiLCJ2YWxpZF91bnRpbCI6MTcwMDAwMDAwMCwic2lnbmF0dXJlIjoiYWJjMTIzLi4uIn0=",
"dashboard_id": "dashboard-789",
"expires_at": 1700000000
}
}
List All Dashboards (Optional)
List all dashboards for the authenticated user’s organization.@router.get("/")
def list_dashboards(
current_user: Dict = Depends(get_authenticated_user),
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100
):
"""List all dashboards for user's organization"""
organization_id = current_user.get("organization_id")
handler = DashboardHandler(db)
dashboards = handler.list_dashboards(
organization_id=organization_id,
skip=skip,
limit=limit
)
return {
"success": True,
"data": dashboards,
"total": len(dashboards)
}
router.get("/dashboards", authenticateUser, async (req, res) => {
const organizationId = req.user.organizationId;
const { skip = 0, limit = 100 } = req.query;
const dashboards = await db.tenantDashboards.findAll({
where: { organization_id: organizationId },
offset: parseInt(skip),
limit: parseInt(limit),
});
res.json({
success: true,
data: dashboards,
total: dashboards.length,
});
});
Delete Dashboard (Optional)
Delete a dashboard for a tenant.@router.delete("/{tenant_id}")
async def delete_dashboard(
tenant_id: str,
current_user: Dict = Depends(get_authenticated_user),
db: Session = Depends(get_db)
):
"""Delete dashboard for a tenant"""
# Verify access
if not has_access(current_user, tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
handler = DashboardHandler(db)
success = await handler.delete_dashboard(tenant_id)
if not success:
raise HTTPException(status_code=404, detail="Dashboard not found")
return {
"success": True,
"message": "Dashboard deleted successfully"
}
router.delete("/dashboards/:tenantId", authenticateUser, async (req, res) => {
const { tenantId } = req.params;
// Verify access
if (!hasAccess(req.user, tenantId)) {
return res.status(403).json({ error: "Access denied" });
}
const deleted = await db.tenantDashboards.destroy({
where: { tenant_id: tenantId },
});
if (!deleted) {
return res.status(404).json({ error: "Dashboard not found" });
}
res.json({
success: true,
message: "Dashboard deleted successfully",
});
});
Error Handling
Implement consistent error handling across all endpoints:// Success Response
{
"success": true,
"data": { ... }
}
// Error Response
{
"success": false,
"error": "Error message here",
"code": "ERROR_CODE"
}
Common Error Codes
| Code | Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid authentication |
FORBIDDEN | 403 | User doesn’t have access to resource |
NOT_FOUND | 404 | Resource not found |
VALIDATION_ERROR | 422 | Invalid request parameters |
SERVER_ERROR | 500 | Internal server error |
Rate Limiting
Implement rate limiting to prevent abuse:from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@router.post("/iframe-token")
@limiter.limit("10/minute")
async def create_iframe_token(...):
# ... implementation
import rateLimit from "express-rate-limit";
const tokenLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
message: "Too many token requests, please try again later",
});
router.post(
"/dashboards/iframe-token",
tokenLimiter,
authenticateUser,
async (req, res) => {
// ... implementation
}
);
Next Steps
Security Best Practices
Learn about security considerations for production
Frontend Setup
Set up the frontend to embed dashboards

