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 = `getDomains${process.env.LOOPIA_USER}${process.env.LOOPIA_PASSWORD}`; 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;