Files
sloth-manager/backend/src/routes/health.js
T
2026-06-02 01:00:27 +02:00

148 lines
5.7 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { requireAuth } = require('../auth');
router.use(requireAuth);
async function checkCloudflare() {
const start = Date.now();
const res = await fetch('https://api.cloudflare.com/client/v4/zones?per_page=1', {
headers: { Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}` },
});
const data = await res.json();
if (!data.success) throw new Error(data.errors?.[0]?.message ?? 'Auth failed');
return Date.now() - start;
}
async function checkLoopia() {
const start = Date.now();
const body = `<?xml version="1.0" encoding="UTF-8"?><methodCall><methodName>getDomains</methodName><params><param><value><string>${process.env.LOOPIA_USER}</string></value></param><param><value><string>${process.env.LOOPIA_PASSWORD}</string></value></param></params></methodCall>`;
const res = await fetch('https://api.loopia.se/RPCSERV', {
method: 'POST',
headers: { 'Content-Type': 'text/xml; charset=utf-8' },
body,
});
const text = await res.text();
if (text.includes('AUTH_ERROR')) throw new Error('Authentication failed');
if (text.includes('faultCode')) throw new Error('API returned a fault');
return Date.now() - start;
}
async function checkPihole() {
const start = Date.now();
const base = process.env.PIHOLE_URL.replace(/\/$/, '');
const res = await fetch(`${base}/api/auth`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: process.env.PIHOLE_PASSWORD }),
});
const data = await res.json();
if (!data.session?.valid) throw new Error('Authentication failed');
// Log out to clean up the session
try {
await fetch(`${base}/api/auth`, {
method: 'DELETE',
headers: { 'X-FTL-SID': data.session.sid },
});
} catch { /* ignore logout errors */ }
return Date.now() - start;
}
async function checkAzure() {
const start = Date.now();
const { AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID } = process.env;
const tokenRes = await fetch(
`https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token`,
{
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: AZURE_CLIENT_ID,
client_secret: AZURE_CLIENT_SECRET,
scope: 'https://management.azure.com/.default',
}),
}
);
const tokenData = await tokenRes.json();
if (tokenData.error) throw new Error(tokenData.error_description ?? tokenData.error);
// Quick check: list DNS zones
const zonesRes = await fetch(
`https://management.azure.com/subscriptions/${AZURE_SUBSCRIPTION_ID}/providers/Microsoft.Network/dnsZones?api-version=2018-05-01`,
{ headers: { Authorization: `Bearer ${tokenData.access_token}` } }
);
const zonesData = await zonesRes.json();
if (zonesData.error) throw new Error(zonesData.error.message ?? 'API error');
return Date.now() - start;
}
async function checkCpanel() {
const https = require('https');
const start = Date.now();
const insecure = process.env.CPANEL_INSECURE === 'true';
await new Promise((resolve, reject) => {
const url = new URL(`${process.env.CPANEL_URL.replace(/\/$/, '')}/execute/DNS/list_zones`);
const lib = url.protocol === 'https:' ? https : require('http');
const req = lib.request(
{
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'GET',
headers: { Authorization: `cpanel ${process.env.CPANEL_USERNAME}:${process.env.CPANEL_API_TOKEN}`, Accept: 'application/json' },
rejectUnauthorized: !insecure,
},
res => {
let body = '';
res.on('data', c => { body += c; });
res.on('end', () => {
try {
const data = JSON.parse(body);
// status 0 with AUTH_ERROR or similar means bad credentials
if (data.status === 0) reject(new Error(data.errors?.join(', ') ?? 'API error'));
else resolve();
} catch {
if (body.trimStart().startsWith('<')) reject(new Error('Received HTML — check credentials or CPANEL_INSECURE setting'));
else reject(new Error('Invalid response'));
}
});
}
);
req.on('error', reject);
req.end();
});
return Date.now() - start;
}
const CHECKS = {
cloudflare: { name: 'Cloudflare', fn: checkCloudflare,
configured: () => !!process.env.CLOUDFLARE_API_TOKEN },
loopia: { name: 'Loopia', fn: checkLoopia,
configured: () => !!(process.env.LOOPIA_USER && process.env.LOOPIA_PASSWORD) },
pihole: { name: 'Pi-hole', fn: checkPihole,
configured: () => !!(process.env.PIHOLE_URL && process.env.PIHOLE_PASSWORD) },
azure: { name: 'Azure DNS', fn: checkAzure,
configured: () => !!(process.env.AZURE_TENANT_ID && process.env.AZURE_CLIENT_ID && process.env.AZURE_CLIENT_SECRET && process.env.AZURE_SUBSCRIPTION_ID) },
cpanel: { name: 'cPanel', fn: checkCpanel,
configured: () => !!(process.env.CPANEL_URL && process.env.CPANEL_USERNAME && process.env.CPANEL_API_TOKEN) },
};
// GET /api/health/providers
router.get('/providers', async (req, res) => {
const results = await Promise.all(
Object.entries(CHECKS).map(async ([id, { name, fn, configured }]) => {
if (!configured()) return { id, name, status: 'unconfigured', latency: null, error: null };
try {
const latency = await fn();
return { id, name, status: 'ok', latency, error: null };
} catch (err) {
return { id, name, status: 'error', latency: null, error: err.message };
}
})
);
res.json(results);
});
module.exports = router;