2026-06-16 15:59:53 -05:00
|
|
|
import { Router } from "express";
|
|
|
|
|
import type { RequestHandler, Router as ExpressRouter } from "express";
|
|
|
|
|
import { apiLogger } from "../../utils/logger.js";
|
|
|
|
|
import { getProxyAgent } from "../../utils/proxy-agent.js";
|
2026-07-19 12:29:52 -05:00
|
|
|
import { createCurrentSettingsRepository } from "../repositories/factory.js";
|
2026-06-16 15:59:53 -05:00
|
|
|
|
|
|
|
|
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 TAILSCALE_API_BASE = "https://api.tailscale.com/api/v2";
|
|
|
|
|
|
|
|
|
|
const router = Router();
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-07-19 12:29:52 -05:00
|
|
|
const apiKey =
|
|
|
|
|
(await createCurrentSettingsRepository().get("tailscale_api_key")) ??
|
|
|
|
|
"";
|
2026-06-16 15:59:53 -05:00
|
|
|
if (!apiKey) {
|
|
|
|
|
return res.json({ devices: [], hasApiKey: false });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const url = `${TAILSCALE_API_BASE}/tailnet/-/devices?fields=all`;
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${apiKey}`,
|
|
|
|
|
"User-Agent": "Termix/1.0",
|
|
|
|
|
},
|
|
|
|
|
dispatcher: getProxyAgent(url),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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: [] });
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
.status(502)
|
|
|
|
|
.json({ error: "Tailscale API error", devices: [] });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: [] });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.use("/tailscale", router);
|
|
|
|
|
}
|