/** * Metabase integration service. * Automatically registers newly-provisioned tenant databases in Metabase * (al crear tenant) y las elimina (al desactivar tenant). Auth via session * token con cache de 13 días (Metabase los expira a 14). * * Variables de entorno (todas opcionales — si METABASE_PASSWORD o * METABASE_PG_PASSWORD faltan, las llamadas se logean y skipean sin romper): * METABASE_URL (default http://192.168.10.170:3000) * METABASE_USERNAME (default ialcarazsalazar@consultoria-as.com) * METABASE_PASSWORD password de la cuenta admin Metabase * METABASE_PG_HOST (default 192.168.10.90) * METABASE_PG_PORT (default 5432) * METABASE_PG_USER (default postgres) * METABASE_PG_PASSWORD password Postgres que Metabase usa para conectar */ const METABASE_URL = process.env.METABASE_URL || 'http://192.168.10.170:3000'; const METABASE_USERNAME = process.env.METABASE_USERNAME || 'ialcarazsalazar@consultoria-as.com'; const METABASE_PASSWORD = process.env.METABASE_PASSWORD || ''; // PostgreSQL connection details exposed to Metabase const PG_HOST = process.env.METABASE_PG_HOST || '192.168.10.90'; const PG_PORT = parseInt(process.env.METABASE_PG_PORT || '5432', 10); const PG_USER = process.env.METABASE_PG_USER || 'postgres'; const PG_PASSWORD = process.env.METABASE_PG_PASSWORD || ''; let cachedSessionToken: string | null = null; let tokenExpiresAt = 0; async function getSessionToken(): Promise { // Re-use cached token if still valid (Metabase sessions last 2 weeks by default) if (cachedSessionToken && Date.now() < tokenExpiresAt) { return cachedSessionToken; } if (!METABASE_PASSWORD) { console.error('[METABASE] METABASE_PASSWORD not configured'); return null; } try { const res = await fetch(`${METABASE_URL}/api/session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: METABASE_USERNAME, password: METABASE_PASSWORD, }), }); if (!res.ok) { const text = await res.text(); console.error(`[METABASE] Auth failed: ${res.status} ${text}`); return null; } const data = await res.json() as { id?: string }; if (!data.id) { console.error('[METABASE] Auth response missing session id'); return null; } cachedSessionToken = data.id; tokenExpiresAt = Date.now() + 13 * 24 * 60 * 60 * 1000; // 13 days return cachedSessionToken; } catch (err) { console.error('[METABASE] Error fetching session token:', err); return null; } } interface RegisterDatabaseInput { nombre: string; dbName: string; } export async function registerDatabase(input: RegisterDatabaseInput): Promise { const sessionToken = await getSessionToken(); if (!sessionToken) { console.error('[METABASE] Skipping database registration — no session token'); return; } if (!PG_PASSWORD) { console.error('[METABASE] METABASE_PG_PASSWORD not configured'); return; } const payload = { name: input.nombre, engine: 'postgres', details: { host: PG_HOST, port: PG_PORT, dbname: input.dbName, user: PG_USER, password: PG_PASSWORD, ssl: false, 'tunnel-enabled': false, 'advanced-options': false, 'schema-filters-type': 'all', }, auto_run_queries: true, is_full_sync: true, is_on_demand: false, }; try { const res = await fetch(`${METABASE_URL}/api/database`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Metabase-Session': sessionToken, }, body: JSON.stringify(payload), }); if (!res.ok) { const text = await res.text(); // 409 or duplicate name is not fatal — log and continue if (res.status === 400 && text.includes('already exists')) { console.log(`[METABASE] Database "${input.nombre}" already registered`); return; } console.error(`[METABASE] Register database failed: ${res.status} ${text}`); return; } const data = await res.json() as { id?: number }; console.log(`[METABASE] Database "${input.nombre}" registered with id=${data.id}`); } catch (err) { console.error('[METABASE] Error registering database:', err); } } export async function deleteDatabase(databaseName: string): Promise { const sessionToken = await getSessionToken(); if (!sessionToken) { console.error('[METABASE] Skipping database deletion — no session token'); return; } try { // Find database by name const listRes = await fetch(`${METABASE_URL}/api/database`, { headers: { 'X-Metabase-Session': sessionToken }, }); if (!listRes.ok) { console.error(`[METABASE] Failed to list databases: ${listRes.status}`); return; } const listData = await listRes.json() as { data?: Array<{ id: number; name: string; details?: { dbname?: string } }> }; const db = listData.data?.find( (d) => d.details?.dbname === databaseName || d.name.includes(databaseName) ); if (!db) { console.log(`[METABASE] No database found for ${databaseName}`); return; } const deleteRes = await fetch(`${METABASE_URL}/api/database/${db.id}`, { method: 'DELETE', headers: { 'X-Metabase-Session': sessionToken }, }); if (!deleteRes.ok) { console.error(`[METABASE] Delete database failed: ${deleteRes.status}`); return; } console.log(`[METABASE] Database ${db.id} (${databaseName}) deleted`); } catch (err) { console.error('[METABASE] Error deleting database:', err); } }