Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
// Cross-platform Node.js API server for Caddy management
|
|
// Uses Caddy Admin API and Technitium DNS API directly
|
|
// Run with: node caddy-api.js
|
|
|
|
const express = require('express');
|
|
const path = require('path');
|
|
const cors = require('cors');
|
|
const fs = require('fs');
|
|
|
|
const app = express();
|
|
const PORT = 3001;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
app.use(express.static('public'));
|
|
|
|
// Configuration
|
|
const CADDY_ADMIN_API = process.env.CADDY_ADMIN_API || 'http://localhost:2019';
|
|
const DNS_SERVER_API = process.env.DNS_SERVER_API || 'http://192.168.254.204:5380';
|
|
const DNS_API_TOKEN = process.env.TECHNITIUM_API_TOKEN || '';
|
|
|
|
// Helper function to make HTTP requests
|
|
async function makeRequest(url, options = {}) {
|
|
const https = url.startsWith('https:') ? require('https') : require('http');
|
|
const urlObj = new URL(url);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const reqOptions = {
|
|
hostname: urlObj.hostname,
|
|
port: urlObj.port,
|
|
path: urlObj.pathname + urlObj.search,
|
|
method: options.method || 'GET',
|
|
headers: options.headers || {},
|
|
...options
|
|
};
|
|
|
|
const req = https.request(reqOptions, (res) => {
|
|
let data = '';
|
|
res.on('data', (chunk) => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
const parsed = JSON.parse(data);
|
|
resolve({ status: res.statusCode, data: parsed });
|
|
} catch (e) {
|
|
resolve({ status: res.statusCode, data: data });
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
|
|
if (options.body) {
|
|
req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body));
|
|
}
|
|
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// Get current Caddy configuration
|
|
app.get('/api/caddy/config', async (req, res) => {
|
|
try {
|
|
const response = await makeRequest(`${CADDY_ADMIN_API}/config/`);
|
|
|
|
if (response.status === 200) {
|
|
res.json({
|
|
status: 'success',
|
|
config: response.data
|
|
});
|
|
} else {
|
|
res.status(response.status).json({
|
|
status: 'error',
|
|
message: 'Failed to get Caddy configuration',
|
|
details: response.data
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting Caddy config:', error);
|
|
res.status(500).json({
|
|
status: 'error',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get list of services (from apps.json + custom apps)
|
|
app.get('/api/services', async (req, res) => {
|
|
try {
|
|
const servicesPath = path.join(__dirname, '../apps.json');
|
|
|
|
if (fs.existsSync(servicesPath)) {
|
|
const servicesData = fs.readFileSync(servicesPath, 'utf8');
|
|
const services = JSON.parse(servicesData);
|
|
res.json({ status: 'success', services });
|
|
} else {
|
|
res.json({ status: 'success', services: [] });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error reading services:', error);
|
|
res.status(500).json({
|
|
status: 'error',
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add DNS record via Technitium API
|
|
async function addDnsRecord(domain, ipAddress, ttl = 3600) {
|
|
if (!DNS_API_TOKEN) {
|
|
throw new Error('DNS API token not configured. Set TECHNITIUM_API_TOKEN environment variable.');
|
|
}
|
|
|
|
const url = `${DNS_SERVER_API}/api/zones/records/add?token=${DNS_API_TOKEN}&domain=${domain}&type=A&ipAddress=${ipAddress}&ttl=${ttl}`;
|
|
|
|
console.log('Adding DNS record:', { domain, ipAddress, ttl });
|
|
const response = await makeRequest(url);
|
|
|
|
if (response.data.status === 'ok') {
|
|
return { success: true, message: 'DNS record added successfully' };
|
|
} else {
|
|
throw new Error(`DNS API error: ${response.data.message || 'Unknown error'}`);
|
|
}
|
|
}
|
|
|
|
// Delete DNS record via Technitium API
|
|
async function deleteDnsRecord(domain, ipAddress) {
|
|
if (!DNS_API_TOKEN) {
|
|
console.warn('DNS API token not configured. Skipping DNS deletion.');
|
|
return { success: true, message: 'DNS deletion skipped (no token)' };
|
|
}
|
|
|
|
const url = `${DNS_SERVER_API}/api/zones/records/delete?token=${DNS_API_TOKEN}&domain=${domain}&type=A&ipAddress=${ipAddress}`;
|
|
|
|
console.log('Deleting DNS record:', { domain, ipAddress });
|
|
const response = await makeRequest(url);
|
|
|
|
if (response.data.status === 'ok') {
|
|
return { success: true, message: 'DNS record deleted successfully' };
|
|
} else {
|
|
throw new Error(`DNS API error: ${response.data.message || 'Unknown error'}`);
|
|
}
|
|
}
|
|
|
|
// Add route to Caddy via Admin API
|
|
async function addCaddyRoute(domain, upstreamUrl, useTls = true) {
|
|
// Build Caddy route configuration
|
|
const routeConfig = {
|
|
"@id": domain,
|
|
"match": [{
|
|
"host": [domain]
|
|
}],
|
|
"handle": [{
|
|
"handler": "reverse_proxy",
|
|
"upstreams": [{
|
|
"dial": upstreamUrl.replace(/^https?:\/\//, '')
|
|
}]
|
|
}],
|
|
"terminal": true
|
|
};
|
|
|
|
// If using internal TLS, we need to add TLS configuration
|
|
if (useTls) {
|
|
// Caddy handles TLS automatically for matched domains
|
|
// Internal CA is configured in the global Caddyfile
|
|
}
|
|
|
|
console.log('Adding Caddy route:', JSON.stringify(routeConfig, null, 2));
|
|
|
|
// Add the route to the HTTP server
|
|
const response = await makeRequest(`${CADDY_ADMIN_API}/config/apps/http/servers/srv0/routes/0`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(routeConfig)
|
|
});
|
|
|
|
if (response.status === 200 || response.status === 201) {
|
|
return { success: true, message: 'Caddy route added successfully' };
|
|
} else {
|
|
throw new Error(`Caddy API error: ${JSON.stringify(response.data)}`);
|
|
}
|
|
}
|
|
|
|
// Deploy app endpoint - handles DNS and Caddy configuration via APIs
|
|
app.post('/api/apps/deploy', async (req, res) => {
|
|
try {
|
|
const { appId, config } = req.body;
|
|
const { subdomain, ip, createDns, port, sslType, dnsType } = config;
|
|
|
|
console.log('Deploying app:', { appId, config });
|
|
|
|
// Validate required fields
|
|
if (!appId || !subdomain || !ip) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Missing required fields: appId, subdomain, ip'
|
|
});
|
|
}
|
|
|
|
// Build the full domain
|
|
const domain = subdomain.includes('.') ? subdomain : `${subdomain}.sami`;
|
|
const finalPort = port || '80';
|
|
const upstreamUrl = `${ip}:${finalPort}`;
|
|
|
|
// Step 1: Add DNS record if requested (private DNS)
|
|
if (createDns && dnsType === 'private') {
|
|
try {
|
|
await addDnsRecord(domain, ip);
|
|
} catch (dnsError) {
|
|
console.error('DNS creation failed:', dnsError);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: `DNS creation failed: ${dnsError.message}`,
|
|
step: 'dns'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Step 2: Add route to Caddy via Admin API
|
|
try {
|
|
const useTls = sslType === 'internal';
|
|
await addCaddyRoute(domain, upstreamUrl, useTls);
|
|
} catch (caddyError) {
|
|
console.error('Caddy route addition failed:', caddyError);
|
|
|
|
// Rollback DNS if it was created
|
|
if (createDns && dnsType === 'private') {
|
|
try {
|
|
await deleteDnsRecord(domain, ip);
|
|
} catch (rollbackError) {
|
|
console.error('DNS rollback failed:', rollbackError);
|
|
}
|
|
}
|
|
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: `Caddy configuration failed: ${caddyError.message}`,
|
|
step: 'caddy'
|
|
});
|
|
}
|
|
|
|
// Step 3: Return success response
|
|
res.json({
|
|
success: true,
|
|
message: `App ${appId} deployed successfully`,
|
|
url: `https://${domain}`,
|
|
domain: domain,
|
|
ip: ip,
|
|
port: finalPort,
|
|
containerId: null,
|
|
dnsCreated: createDns && dnsType === 'private',
|
|
caddyConfigured: true
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Deployment error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Delete app endpoint - removes DNS and Caddy configuration
|
|
app.post('/api/apps/delete', async (req, res) => {
|
|
try {
|
|
const { domain, ip } = req.body;
|
|
|
|
if (!domain) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: 'Domain is required'
|
|
});
|
|
}
|
|
|
|
console.log('Deleting app:', { domain, ip });
|
|
|
|
// Step 1: Remove from Caddy
|
|
try {
|
|
const response = await makeRequest(`${CADDY_ADMIN_API}/id/${domain}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.status !== 200) {
|
|
console.warn('Caddy route deletion warning:', response.data);
|
|
}
|
|
} catch (caddyError) {
|
|
console.error('Caddy route deletion failed:', caddyError);
|
|
// Continue anyway to try DNS deletion
|
|
}
|
|
|
|
// Step 2: Remove DNS record if IP provided
|
|
if (ip) {
|
|
try {
|
|
await deleteDnsRecord(domain, ip);
|
|
} catch (dnsError) {
|
|
console.error('DNS deletion failed:', dnsError);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: `DNS deletion failed: ${dnsError.message}`,
|
|
caddyDeleted: true,
|
|
dnsDeleted: false
|
|
});
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'App deleted successfully',
|
|
domain: domain,
|
|
caddyDeleted: true,
|
|
dnsDeleted: !!ip
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Deletion error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Test endpoint
|
|
app.get('/api/caddy/test', (req, res) => {
|
|
res.json({
|
|
status: 'success',
|
|
message: 'Caddy API is running',
|
|
timestamp: new Date().toISOString(),
|
|
platform: process.platform,
|
|
caddyAdminApi: CADDY_ADMIN_API,
|
|
dnsServerApi: DNS_SERVER_API,
|
|
dnsTokenConfigured: !!DNS_API_TOKEN
|
|
});
|
|
});
|
|
|
|
// Health check
|
|
app.get('/health', (req, res) => {
|
|
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log(`\n====================================`);
|
|
console.log(`Caddy API server running on http://localhost:${PORT}`);
|
|
console.log(`====================================`);
|
|
console.log(`Caddy Admin API: ${CADDY_ADMIN_API}`);
|
|
console.log(`DNS Server API: ${DNS_SERVER_API}`);
|
|
console.log(`DNS Token: ${DNS_API_TOKEN ? '✓ Configured' : '✗ Not configured'}`);
|
|
console.log(`\nEndpoints:`);
|
|
console.log(` POST /api/apps/deploy - Deploy an app (DNS + Caddy)`);
|
|
console.log(` POST /api/apps/delete - Delete an app (DNS + Caddy)`);
|
|
console.log(` GET /api/services - Get list of services`);
|
|
console.log(` GET /api/caddy/config - Get current Caddy configuration`);
|
|
console.log(` GET /api/caddy/test - Test API connectivity`);
|
|
console.log(` GET /health - Health check`);
|
|
console.log(`====================================\n`);
|
|
});
|
|
|
|
module.exports = app;
|