Files
dashcaddy/status/js/core/service-infrastructure.js
Sami f2f33b4b40 Make DNS servers fully dynamic from config.json
DNS server IDs (dns1, dns2, dns3) were hardcoded throughout the frontend
and backend. Now config.json's dnsServers object is the single source of
truth — adding or removing a DNS server in config automatically updates
the dashboard cards, credential modal, health checks, and probes.

- credentials.js: rebuild modal sections dynamically from SITE.dnsServers
- globals.js: add getPrimaryDnsId() helper for primary DNS lookups
- service-create.js, service-infrastructure.js: use dynamic DNS ID
- startup-validator.js: dynamic topCardServices from config
- middleware.js: add license endpoints to public routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 16:55:07 -07:00

246 lines
6.9 KiB
JavaScript

// ========== SERVICE INFRASTRUCTURE ==========
// Caddy config generation, DNS record creation, and service registration.
(function () {
// ===== LOAD EXISTING CAs =====
async function loadExistingCAs(caddyfilePath) {
try {
const response = await fetch(`/api/v1/caddy/cas?caddyfilePath=${encodeURIComponent(caddyfilePath)}`);
if (!response.ok) {
throw new Error(`Failed to load CAs: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success') {
const select = document.getElementById('existing-ca-select');
select.innerHTML = '';
if (result.data.cas.length === 0) {
select.innerHTML = '<option value="">No CAs found in Caddyfile</option>';
} else {
select.innerHTML = '<option value="">Select existing CA...</option>';
result.data.cas.forEach(ca => {
const option = document.createElement('option');
if (typeof ca === 'object') {
option.value = ca.id;
option.textContent = ca.displayName || ca.name;
} else {
option.value = ca;
option.textContent = ca;
}
select.appendChild(option);
});
}
return result.data.cas;
} else {
throw new Error(result.message);
}
} catch (error) {
console.error('Error loading CAs:', error);
const select = document.getElementById('existing-ca-select');
select.innerHTML = '<option value="">Error loading CAs</option>';
return [];
}
}
// ===== GENERATE CADDY CONFIG =====
function generateCaddyConfig(config) {
const {
subdomain,
port,
ip,
sslType,
caName,
existingCa,
enableAuth,
enableCors,
customHeaders,
upstreamPath,
healthCheck,
timeout,
tailscaleOnly
} = config;
let caddyConfig = `${buildDomain(subdomain)} {\n`;
// Tailscale-only access restriction
if (tailscaleOnly) {
caddyConfig += ` @blocked not remote_ip 100.64.0.0/10\n`;
caddyConfig += ` respond @blocked "Access denied. Tailscale connection required." 403\n`;
}
// SSL Configuration
switch (sslType) {
case 'letsencrypt':
break;
case 'caddy-managed':
caddyConfig += ` tls internal\n`;
break;
case 'existing-ca':
if (existingCa) {
caddyConfig += ` tls {\n ca ${existingCa}\n }\n`;
}
break;
case 'custom-ca':
if (caName) {
caddyConfig += ` tls {\n ca ${caName}\n }\n`;
}
break;
}
// Authentication
if (enableAuth) {
caddyConfig += ` basicauth {\n admin $2a$14$hashed_password_here\n }\n`;
}
// CORS Headers
if (enableCors) {
caddyConfig += ` header {\n`;
caddyConfig += ` Access-Control-Allow-Origin "*"\n`;
caddyConfig += ` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"\n`;
caddyConfig += ` Access-Control-Allow-Headers "Content-Type, Authorization"\n`;
caddyConfig += ` }\n`;
}
// Custom Headers
if (customHeaders) {
try {
const headers = JSON.parse(customHeaders);
caddyConfig += ` header {\n`;
Object.entries(headers).forEach(([key, value]) => {
caddyConfig += ` ${key} "${value}"\n`;
});
caddyConfig += ` }\n`;
} catch (e) {
console.warn('Invalid JSON in custom headers');
}
}
// Health Check
if (healthCheck) {
caddyConfig += ` health_uri ${healthCheck}\n`;
}
// Reverse Proxy
caddyConfig += ` reverse_proxy ${ip}:${port} {\n`;
if (upstreamPath && upstreamPath !== '/') {
caddyConfig += ` rewrite ${upstreamPath}\n`;
}
if (timeout && timeout !== 30) {
caddyConfig += ` transport http {\n`;
caddyConfig += ` dial_timeout ${timeout}s\n`;
caddyConfig += ` response_header_timeout ${timeout}s\n`;
caddyConfig += ` }\n`;
}
caddyConfig += ` }\n`;
caddyConfig += `}\n`;
return caddyConfig;
}
// ===== CREATE DNS RECORD =====
async function createDnsRecord(subdomain, ip, ttl = DC.DEFAULTS.TTL) {
const dnsToken = window.getToken(getPrimaryDnsId(), 'admin');
if (!dnsToken) {
throw new Error('DNS admin token not configured. Please set it in the Tokens menu.');
}
const domain = buildDomain(subdomain);
const response = await secureFetch('/api/v1/dns/record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
domain: domain,
ip: ip,
ttl: ttl,
token: dnsToken,
server: SITE.dnsIp
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`DNS API Error: ${response.status} - ${errorText}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(`DNS Error: ${result.error || 'Unknown error'}`);
}
return result;
}
// ===== ADD SERVICE TO CONFIG =====
async function addServiceToConfig(serviceConfig) {
const newService = {
id: serviceConfig.subdomain,
name: serviceConfig.name,
logo: serviceConfig.logo || `/assets/${serviceConfig.subdomain}.png`
};
try {
const response = await secureFetch('/api/v1/services', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newService)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save service');
}
await window.loadServices();
window.buildGrid();
return newService;
} catch (error) {
console.error('Failed to add service to config:', error);
throw error;
}
}
// ===== ADD TO CADDYFILE =====
async function addToCaddyfile(config) {
const subdomain = document.getElementById('service-subdomain-input').value.trim();
const ip = document.getElementById('service-ip-input').value.trim() || 'localhost';
const port = document.getElementById('service-port-input').value.trim() || '80';
const response = await secureFetch('/api/v1/site', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
domain: buildDomain(subdomain),
upstream: `${ip}:${port}`,
config: config
})
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.error || `Caddy API Error: ${response.status}`);
}
return result;
}
// ===== WINDOW EXPORTS =====
window.loadExistingCAs = loadExistingCAs;
window.generateCaddyConfig = generateCaddyConfig;
window.createDnsRecord = createDnsRecord;
window.addServiceToConfig = addServiceToConfig;
window.addToCaddyfile = addToCaddyfile;
})();