feat: Superset gateway — proxy /superset/*, guest-token, dashboards API
This commit is contained in:
parent
98b79be03d
commit
5571cb51e6
|
|
@ -48,6 +48,8 @@ import xosRoutes from "./xos/routes";
|
||||||
import governanceRoutes from "./governance/routes";
|
import governanceRoutes from "./governance/routes";
|
||||||
import { setupPlusProxy } from "./plus/proxy";
|
import { setupPlusProxy } from "./plus/proxy";
|
||||||
import { setupMetabaseProxy } from "./metabase/proxy";
|
import { setupMetabaseProxy } from "./metabase/proxy";
|
||||||
|
import { setupSupersetProxy } from "./superset/proxy";
|
||||||
|
import { registerSupersetRoutes } from "./superset/routes";
|
||||||
import { registerEngineRoomRoutes } from "./engine-room/routes";
|
import { registerEngineRoomRoutes } from "./engine-room/routes";
|
||||||
import { registerMetaSetRoutes } from "./metaset/routes";
|
import { registerMetaSetRoutes } from "./metaset/routes";
|
||||||
import plusSsoRoutes from "./plus/sso";
|
import plusSsoRoutes from "./plus/sso";
|
||||||
|
|
@ -70,6 +72,8 @@ export async function registerRoutes(
|
||||||
// Arcádia Plus - Proxy registered AFTER session but BEFORE auth-protected routes
|
// Arcádia Plus - Proxy registered AFTER session but BEFORE auth-protected routes
|
||||||
await setupPlusProxy(app);
|
await setupPlusProxy(app);
|
||||||
|
|
||||||
|
// Superset BI - Proxy
|
||||||
|
setupSupersetProxy(app);
|
||||||
// Metabase BI - Proxy to Metabase instance
|
// Metabase BI - Proxy to Metabase instance
|
||||||
setupMetabaseProxy(app);
|
setupMetabaseProxy(app);
|
||||||
|
|
||||||
|
|
@ -87,6 +91,7 @@ export async function registerRoutes(
|
||||||
app.use("/api/graph", graphRoutes);
|
app.use("/api/graph", graphRoutes);
|
||||||
registerBiEngineRoutes(app);
|
registerBiEngineRoutes(app);
|
||||||
registerMetaSetRoutes(app);
|
registerMetaSetRoutes(app);
|
||||||
|
registerSupersetRoutes(app);
|
||||||
registerCommEngineRoutes(app);
|
registerCommEngineRoutes(app);
|
||||||
registerLearningRoutes(app);
|
registerLearningRoutes(app);
|
||||||
app.use("/api/compass", compassRoutes);
|
app.use("/api/compass", compassRoutes);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Express, Response } from "express";
|
||||||
|
import { createProxyMiddleware } from "http-proxy-middleware";
|
||||||
|
|
||||||
|
const SUPERSET_HOST = process.env.SUPERSET_HOST || "superset";
|
||||||
|
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
|
||||||
|
const SUPERSET_TIMEOUT = 60000;
|
||||||
|
|
||||||
|
export function setupSupersetProxy(app: Express): void {
|
||||||
|
const target = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
|
||||||
|
|
||||||
|
const supersetProxy = createProxyMiddleware({
|
||||||
|
target,
|
||||||
|
changeOrigin: true,
|
||||||
|
timeout: SUPERSET_TIMEOUT,
|
||||||
|
proxyTimeout: SUPERSET_TIMEOUT,
|
||||||
|
pathRewrite: { "^/superset": "" },
|
||||||
|
on: {
|
||||||
|
error: (err, _req, res) => {
|
||||||
|
console.error("[Superset Proxy] Error:", err.message);
|
||||||
|
if (res && typeof (res as Response).status === "function") {
|
||||||
|
(res as Response).status(502).json({
|
||||||
|
error: "Superset indisponível",
|
||||||
|
message: "O Superset está iniciando. Tente novamente em alguns segundos.",
|
||||||
|
target,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
proxyRes: (proxyRes) => {
|
||||||
|
const location = proxyRes.headers["location"];
|
||||||
|
if (location && typeof location === "string" && location.startsWith("/")) {
|
||||||
|
proxyRes.headers["location"] = `/superset${location}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/superset", supersetProxy);
|
||||||
|
console.log(`[Superset Proxy] Configurado -> /superset/* => ${target}`);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
import type { Express, Request, Response } from "express";
|
||||||
|
|
||||||
|
const SUPERSET_HOST = process.env.SUPERSET_HOST || "superset";
|
||||||
|
const SUPERSET_PORT = parseInt(process.env.SUPERSET_PORT || "8088", 10);
|
||||||
|
const SUPERSET_URL = `http://${SUPERSET_HOST}:${SUPERSET_PORT}`;
|
||||||
|
const ADMIN_USER = process.env.SUPERSET_ADMIN_USERNAME || "admin";
|
||||||
|
const ADMIN_PASS = process.env.SUPERSET_ADMIN_PASSWORD || "arcadia1337";
|
||||||
|
|
||||||
|
// Cache do service token (expira em 50 min)
|
||||||
|
let serviceToken: string | null = null;
|
||||||
|
let serviceTokenExpiry = 0;
|
||||||
|
|
||||||
|
async function getServiceToken(): Promise<string> {
|
||||||
|
if (serviceToken && Date.now() < serviceTokenExpiry) return serviceToken;
|
||||||
|
|
||||||
|
const resp = await fetch(`${SUPERSET_URL}/api/v1/security/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: ADMIN_USER,
|
||||||
|
password: ADMIN_PASS,
|
||||||
|
provider: "db",
|
||||||
|
refresh: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.text().catch(() => "");
|
||||||
|
throw new Error(`Falha ao autenticar no Superset: ${resp.status} ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
serviceToken = data.access_token;
|
||||||
|
serviceTokenExpiry = Date.now() + 50 * 60 * 1000;
|
||||||
|
return serviceToken!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function supersetFetch(path: string, options: RequestInit = {}): Promise<any> {
|
||||||
|
const token = await getServiceToken();
|
||||||
|
const resp = await fetch(`${SUPERSET_URL}${path}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
...(options.headers || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.status === 401) {
|
||||||
|
// Token expirado — forçar renovação
|
||||||
|
serviceToken = null;
|
||||||
|
serviceTokenExpiry = 0;
|
||||||
|
return supersetFetch(path, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.text().catch(() => "");
|
||||||
|
throw new Error(`Superset API ${resp.status}: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.json().catch(() => ({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerSupersetRoutes(app: Express): void {
|
||||||
|
|
||||||
|
// Guest Token — para embedding em qualquer tela React
|
||||||
|
app.post("/api/superset/guest-token", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||||
|
|
||||||
|
const { dashboardId } = req.body;
|
||||||
|
if (!dashboardId) return res.status(400).json({ error: "dashboardId obrigatório" });
|
||||||
|
|
||||||
|
const token = await getServiceToken();
|
||||||
|
const user = req.user as any;
|
||||||
|
|
||||||
|
const guestResp = await fetch(`${SUPERSET_URL}/api/v1/security/guest_token/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user: {
|
||||||
|
username: `arcadia_${user?.id || "guest"}`,
|
||||||
|
first_name: user?.name?.split(" ")[0] || "User",
|
||||||
|
last_name: user?.name?.split(" ").slice(1).join(" ") || "",
|
||||||
|
},
|
||||||
|
resources: [{ type: "dashboard", id: dashboardId }],
|
||||||
|
rls: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!guestResp.ok) {
|
||||||
|
const err = await guestResp.text();
|
||||||
|
return res.status(502).json({ error: "Falha ao gerar guest token", detail: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { token: guestToken } = await guestResp.json();
|
||||||
|
res.json({ token: guestToken, supersetUrl: "/superset" });
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(502).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lista dashboards
|
||||||
|
app.get("/api/superset/dashboards", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||||
|
const data = await supersetFetch(
|
||||||
|
"/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page_size:50)"
|
||||||
|
);
|
||||||
|
res.json(data.result || []);
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(502).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lista charts
|
||||||
|
app.get("/api/superset/charts", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
||||||
|
const data = await supersetFetch("/api/v1/chart/?q=(page_size:50)");
|
||||||
|
res.json(data.result || []);
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(502).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get("/api/superset/health", async (_req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${SUPERSET_URL}/health`);
|
||||||
|
res.json({ online: resp.ok, url: SUPERSET_URL });
|
||||||
|
} catch {
|
||||||
|
res.json({ online: false, url: SUPERSET_URL });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manter compatibilidade com rotas antigas do MetaSet
|
||||||
|
app.get("/api/bi/metaset/health", async (_req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${SUPERSET_URL}/health`);
|
||||||
|
res.json({ service: "Superset", online: resp.ok });
|
||||||
|
} catch {
|
||||||
|
res.json({ service: "Superset", online: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[Superset] Rotas registradas em /api/superset/*");
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue