import { Router, type RequestHandler, type Router as ExpressRouter, } from "express"; import { apiLogger } from "../../utils/logger.js"; import { fetchWithProxy } from "../../utils/proxy-agent.js"; import { createCurrentSettingsRepository } from "../repositories/factory.js"; interface TailscaleDevice { id: string; name: string; hostname: string; addresses: string[]; os: string; lastSeen: string; } interface TailscaleAPIDevice { id: string; name: string; hostname: string; addresses: string[]; os: string; lastSeen: string; nodeId?: string; } const DEFAULT_TAILSCALE_API_BASE = "https://api.tailscale.com/api/v2"; const router = Router(); function normalizeApiBase(raw: string | null): string { const trimmed = (raw ?? "").trim().replace(/\/+$/, ""); return trimmed || DEFAULT_TAILSCALE_API_BASE; } export function registerTailscaleRoutes( app: ExpressRouter, authenticateJWT: RequestHandler, ): void { /** * @openapi * /tailscale/devices: * get: * summary: List Tailscale devices * description: Returns the list of devices in the configured tailnet using the stored API key. * tags: * - Tailscale * responses: * 200: * description: List of tailnet devices. * content: * application/json: * schema: * type: object * properties: * devices: * type: array * items: * type: object * 500: * description: Failed to fetch Tailscale devices. */ router.get("/devices", authenticateJWT, async (_req, res) => { try { const settingsRepo = createCurrentSettingsRepository(); const apiKey = (await settingsRepo.get("tailscale_api_key")) ?? ""; if (!apiKey) { return res.json({ devices: [], hasApiKey: false }); } const apiBase = normalizeApiBase( await settingsRepo.get("tailscale_api_base_url"), ); const url = `${apiBase}/tailnet/-/devices?fields=all`; const response = await fetchWithProxy(url, { headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": "Termix/1.0", }, }); if (!response.ok) { apiLogger.warn("Tailscale API returned non-OK status", { operation: "tailscale_devices", status: response.status, }); if (response.status === 401 || response.status === 403) { return res.status(401).json({ error: "Invalid Tailscale API key", devices: [], hasApiKey: true, }); } return res.status(502).json({ error: "Tailscale API error", devices: [], hasApiKey: true, }); } const data = (await response.json()) as { devices: TailscaleAPIDevice[] }; const devices: TailscaleDevice[] = (data.devices ?? []).map((d) => ({ id: d.id, name: d.name, hostname: d.hostname, addresses: d.addresses ?? [], os: d.os, lastSeen: d.lastSeen, })); res.json({ devices, hasApiKey: true }); } catch (err) { apiLogger.error("Failed to fetch Tailscale devices", err, { operation: "tailscale_devices", }); res.status(500).json({ error: "Failed to fetch Tailscale devices", devices: [], hasApiKey: true, }); } }); app.use("/tailscale", router); }