"use server"; if (typeof globalThis.crypto === 'undefined') { globalThis.crypto = require('crypto'); } import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; import jwt from "jsonwebtoken"; import mongoose from "mongoose"; const BASE_URL = process.env.BACKONE_BASE_URL || process.env.NETIFY_BASE_URL; const API_URL = BASE_URL ? `${BASE_URL}/assets/agents` : "https://manager.netify.ai/api/v1/assets/agents"; const getApiKey = () => process.env.BACKONE_API_KEY || process.env.NETIFY_API_KEY; const getJwtToken = () => process.env.BACKONE_JWT_TOKEN || process.env.NETIFY_JWT_TOKEN; const getSiteUuid = () => process.env.BACKONE_SITE_UUID || process.env.NETIFY_SITE_UUID; // Helper function to get headers const getHeaders = () => { const API_KEY = getApiKey(); const JWT_TOKEN = getJwtToken(); const SITE_UUID = getSiteUuid(); if (!API_KEY && !JWT_TOKEN) { throw new Error("BACKONE_API_KEY or BACKONE_JWT_TOKEN is not set in environment variables"); } const headers: Record = { "Content-Type": "application/json", }; // Prioritize long-term API Key as recommended by the DPI API support team if (API_KEY) { headers["x-api-key"] = API_KEY; } else if (JWT_TOKEN) { headers["x-api-key"] = JWT_TOKEN; } if (SITE_UUID) { headers["x-net-site"] = SITE_UUID; } return headers; }; export interface Agent { id: number; uuid: string; serial: string; site_uuid: string; organization_uuid: string; provisioned: boolean; activated: boolean; label?: string; created_at: { human: string; date: string; unix_time: number }; updated_at: { human: string; date: string; unix_time: number }; last_seen_at: { human: string; date: string; unix_time: number }; } export async function getAgents(clientSiteUuid?: string): Promise { try { const API_KEY = getApiKey(); let SITE_UUID = clientSiteUuid || getSiteUuid(); // Securely retrieve the user's actual role and site_uuid on the server side try { const cookieStore = await cookies(); const token = cookieStore.get('token')?.value; if (token) { const decoded: any = jwt.verify(token, process.env.JWT_SECRET || 'super-secret-backone-key'); if (decoded && decoded.role === 'TENANT_ADMIN') { // Force user's site_uuid to prevent TENANT_ADMIN from requesting other sites SITE_UUID = decoded.site_uuid; } } } catch (cookieErr) { console.warn("Failed to retrieve or verify cookie token inside getAgents:", cookieErr); } // All agent data comes from MongoDB (single source of truth per Rule 13) if (API_KEY && API_KEY.startsWith("sk_db_")) { const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/backone_dpi'; // Connect to MongoDB if not already connected if (mongoose.connection.readyState === 0) { await mongoose.connect(MONGODB_URI); } const db = mongoose.connection.db; if (!db) { throw new Error("Database connection not ready"); } const filter: any = { agent_uuid: { $ne: null } }; // exclude site-level aggregate rows if (SITE_UUID) { filter.site_uuid = SITE_UUID; } // Get all agents from MongoDB summaries collection, deduplicated by agent_uuid const agentSummaries = await db.collection('summaries') .aggregate([ { $match: filter }, { $sort: { timestamp: -1 } }, { $group: { _id: '$agent_uuid', lastSeen: { $first: '$timestamp' }, label: { $first: '$agent_label' }, site_uuid: { $first: '$site_uuid' }, // capture the site this agent belongs to } }, // Final guard: only keep agents whose primary site matches the queried site ...(SITE_UUID ? [{ $match: { site_uuid: SITE_UUID } }] : []), ]).toArray(); // Check for any flows recorded in the last 12 hours to determine online status const twelveHoursAgo = new Date(Date.now() - 12 * 3600000); const activeAgents = await db.collection('flows').distinct('agent_uuid', { timestamp: { $gte: twelveHoursAgo } }); const activeAgentsSet = new Set(activeAgents); const agents: Agent[] = agentSummaries.map((item: any, idx: number) => { const lastSeenDate = item.lastSeen ? new Date(item.lastSeen) : null; const isOnline = activeAgentsSet.has(item._id); const statusHuman = isOnline ? 'Online' : lastSeenDate ? `Last Seen ${lastSeenDate.toLocaleString('id-ID', { timeZone: 'Asia/Jakarta', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })} WIB` : 'Offline'; return { id: idx + 1, uuid: item._id, serial: item._id, site_uuid: SITE_UUID || '', organization_uuid: '', provisioned: true, activated: isOnline, label: item.label || item._id, created_at: { human: 'N/A', date: '', unix_time: 0 }, updated_at: { human: 'N/A', date: '', unix_time: 0 }, last_seen_at: { human: statusHuman, date: lastSeenDate?.toISOString() || '', unix_time: lastSeenDate ? lastSeenDate.getTime() / 1000 : 0, }, }; }); return agents; } const response = await fetch(API_URL, { method: "GET", headers: getHeaders(), cache: "no-store", }); if (!response.ok) { if (response.status === 403) { throw new Error("Asset Management Disabled: The configured API key does not have permission to manage provisioning agents (403 Forbidden)."); } throw new Error(`Failed to fetch agents: ${response.statusText} (${response.status})`); } const result = await response.json(); if (result.status_code !== 0) { throw new Error(result.status_message || "Unknown API error"); } return result.data || []; } catch (error: any) { console.error("Error fetching agents:", error); throw new Error(error.message || "Failed to retrieve agents inventory."); } } export async function getAgent(agentId: string): Promise { try { const API_KEY = getApiKey(); if (API_KEY && API_KEY.startsWith("sk_db_")) { const agents = await getAgents(); return agents.find(a => a.uuid === agentId || String(a.id) === agentId) || null; } const response = await fetch(`${API_URL}/${agentId}`, { method: "GET", headers: getHeaders(), cache: "no-store", }); if (!response.ok) { throw new Error(`Failed to fetch agent ${agentId}: ${response.statusText}`); } const result = await response.json(); return result.data; } catch (error) { console.error(`Error fetching agent ${agentId}:`, error); return null; } } export async function createAgent(agentId: string, label: string) { try { const API_KEY = getApiKey(); if (API_KEY && API_KEY.startsWith("sk_db_")) { return { success: false, message: "Action denied: Site-scoped API Key is not allowed to create Agents." }; } const url = agentId ? `${API_URL}/${encodeURIComponent(agentId)}?label=${encodeURIComponent(label)}` : `${API_URL}?label=${encodeURIComponent(label)}`; const response = await fetch(url, { method: "POST", headers: getHeaders(), }); const result = await response.json(); if (!response.ok || result.status_code !== 0) { return { success: false, message: result.status_message || response.statusText }; } revalidatePath('/agents'); return { success: true, data: result.data }; } catch (error: any) { return { success: false, message: error.message }; } } export async function updateAgent(agentId: string, label: string) { try { const API_KEY = getApiKey(); if (API_KEY && API_KEY.startsWith("sk_db_")) { return { success: false, message: "Action denied: Site-scoped API Key is not allowed to modify Agents." }; } const response = await fetch(`${API_URL}/${encodeURIComponent(agentId)}?label=${encodeURIComponent(label)}`, { method: "PATCH", headers: getHeaders(), }); const result = await response.json(); if (!response.ok || result.status_code !== 0) { return { success: false, message: result.status_message || response.statusText }; } revalidatePath('/agents'); return { success: true, data: result.data }; } catch (error: any) { return { success: false, message: error.message }; } } export async function deleteAgent(agentId: string) { try { const API_KEY = getApiKey(); if (API_KEY && API_KEY.startsWith("sk_db_")) { return { success: false, message: "Action denied: Site-scoped API Key is not allowed to delete Agents." }; } const response = await fetch(`${API_URL}/${encodeURIComponent(agentId)}`, { method: "DELETE", headers: getHeaders(), }); const result = await response.json(); if (!response.ok || result.status_code !== 0) { return { success: false, message: result.status_message || response.statusText }; } revalidatePath('/agents'); return { success: true }; } catch (error: any) { return { success: false, message: error.message }; } }