Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { DEFAULT_PORTS } = require('../shared/constants');
|
||||
|
||||
/**
|
||||
* CaddyfileGenerator - Creates Caddyfile and docker-compose.yml for DashCaddy
|
||||
*
|
||||
* Generates production-grade configs that match the patterns used by the
|
||||
* running DashCaddy deployment (CORS snippets, admin origins, PKI, etc.)
|
||||
*/
|
||||
class CaddyfileGenerator {
|
||||
/**
|
||||
* Normalize a Windows path to forward slashes for Caddyfile/Docker Compose
|
||||
*/
|
||||
_p(s) {
|
||||
return s.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the CORS snippet block used by the API
|
||||
*/
|
||||
_corsSnippets() {
|
||||
return `(cors-preflight) {
|
||||
@preflight method OPTIONS
|
||||
header @preflight {
|
||||
Access-Control-Allow-Origin "*"
|
||||
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
Access-Control-Allow-Headers "*"
|
||||
Access-Control-Max-Age "600"
|
||||
}
|
||||
respond @preflight 204
|
||||
}
|
||||
(cors-allow) {
|
||||
header {
|
||||
Access-Control-Allow-Origin "*"
|
||||
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
Access-Control-Allow-Headers "*"
|
||||
Access-Control-Max-Age "600"
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the dashcaddy_auth snippet for SSO
|
||||
*/
|
||||
_authSnippet(apiPort) {
|
||||
return `# DashCaddy SSO auth snippet
|
||||
(dashcaddy_auth) {
|
||||
forward_auth localhost:${apiPort} {
|
||||
uri /api/auth/gate/{args[0]}
|
||||
copy_headers Authorization X-Api-Key X-App-Cookie X-Emby-Token X-Plex-Token
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the dashboard site block content (shared across all modes)
|
||||
*/
|
||||
_dashboardBlock(dashboardPath, tier, apiPort) {
|
||||
let block = '';
|
||||
|
||||
block += ` root * ${this._p(dashboardPath)}\n`;
|
||||
block += ` encode gzip\n\n`;
|
||||
|
||||
// API proxy for intermediate/advanced tiers
|
||||
if (tier !== 'basic') {
|
||||
block += ` # API proxy to Docker container\n`;
|
||||
block += ` handle /api/* {\n`;
|
||||
block += ` reverse_proxy localhost:${apiPort}\n`;
|
||||
block += ` }\n\n`;
|
||||
}
|
||||
|
||||
// SPA fallback with file_server
|
||||
block += ` # Static site + SPA fallback\n`;
|
||||
block += ` handle {\n`;
|
||||
block += ` @notFile not file {path}\n`;
|
||||
block += ` rewrite @notFile /index.html\n`;
|
||||
block += ` file_server\n`;
|
||||
block += ` }\n`;
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Caddyfile for local mode (IP:Port, no TLS)
|
||||
*/
|
||||
generateCaddyfile(options) {
|
||||
const {
|
||||
dashboardPath,
|
||||
port = 8080,
|
||||
tier = 'basic',
|
||||
apiPort = DEFAULT_PORTS.API
|
||||
} = options;
|
||||
|
||||
const adminPort = DEFAULT_PORTS.CADDY_ADMIN;
|
||||
|
||||
let caddyfile = `# DashCaddy Caddyfile - Local Mode
|
||||
# Generated by DashCaddy Installer
|
||||
|
||||
{
|
||||
admin localhost:${adminPort} {
|
||||
origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort}
|
||||
}
|
||||
auto_https off
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
if (tier !== 'basic') {
|
||||
caddyfile += this._corsSnippets() + '\n';
|
||||
}
|
||||
|
||||
caddyfile += `:${port} {\n`;
|
||||
caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort);
|
||||
caddyfile += `}\n`;
|
||||
|
||||
return caddyfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Caddyfile for public domain mode (Let's Encrypt TLS)
|
||||
*/
|
||||
generateCaddyfileWithDomain(options) {
|
||||
const {
|
||||
dashboardPath,
|
||||
domain,
|
||||
email = '',
|
||||
tier = 'basic',
|
||||
apiPort = DEFAULT_PORTS.API
|
||||
} = options;
|
||||
|
||||
const adminPort = DEFAULT_PORTS.CADDY_ADMIN;
|
||||
|
||||
let caddyfile = `# DashCaddy Caddyfile - Public Domain Mode
|
||||
# Generated by DashCaddy Installer
|
||||
|
||||
{
|
||||
admin localhost:${adminPort} {
|
||||
origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort}
|
||||
}
|
||||
`;
|
||||
if (email) {
|
||||
caddyfile += ` email ${email}\n`;
|
||||
}
|
||||
caddyfile += `}\n\n`;
|
||||
|
||||
if (tier !== 'basic') {
|
||||
caddyfile += this._corsSnippets() + '\n';
|
||||
caddyfile += this._authSnippet(apiPort) + '\n';
|
||||
}
|
||||
|
||||
caddyfile += `${domain} {\n`;
|
||||
caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort);
|
||||
caddyfile += `}\n`;
|
||||
|
||||
return caddyfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Caddyfile for custom TLD mode (internal CA)
|
||||
*/
|
||||
generateCaddyfileCustomTLD(options) {
|
||||
const {
|
||||
dashboardPath,
|
||||
installPath,
|
||||
tld = '.home',
|
||||
caName = 'DashCaddy Local CA',
|
||||
tier = 'basic',
|
||||
apiPort = DEFAULT_PORTS.API
|
||||
} = options;
|
||||
|
||||
const adminPort = DEFAULT_PORTS.CADDY_ADMIN;
|
||||
const dashboardDomain = `dashcaddy${tld}`;
|
||||
const certsPath = installPath ? this._p(path.join(installPath, 'certs')) : './certs';
|
||||
|
||||
let caddyfile = `# DashCaddy Caddyfile - Custom TLD Mode (${tld})
|
||||
# Generated by DashCaddy Installer
|
||||
|
||||
{
|
||||
storage file_system {
|
||||
root ${certsPath}
|
||||
}
|
||||
|
||||
admin localhost:${adminPort} {
|
||||
origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort}
|
||||
}
|
||||
|
||||
pki {
|
||||
ca local {
|
||||
name "${caName}"
|
||||
root_cn "${caName} Root CA"
|
||||
intermediate_cn "${caName} Intermediate CA"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
if (tier !== 'basic') {
|
||||
caddyfile += this._corsSnippets() + '\n';
|
||||
caddyfile += this._authSnippet(apiPort) + '\n';
|
||||
}
|
||||
|
||||
caddyfile += `# Dashboard - ${dashboardDomain}\n`;
|
||||
caddyfile += `${dashboardDomain} {\n`;
|
||||
caddyfile += ` tls internal\n\n`;
|
||||
caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort);
|
||||
caddyfile += `}\n`;
|
||||
|
||||
return caddyfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write Caddyfile to disk
|
||||
*/
|
||||
async writeCaddyfile(content, outputPath) {
|
||||
try {
|
||||
await fs.promises.writeFile(outputPath, content, 'utf8');
|
||||
return { success: true, path: outputPath };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create complete Caddyfile setup
|
||||
* @param {string} installPath - Installation directory
|
||||
* @param {Object} options - Configuration options
|
||||
*/
|
||||
async createCaddyfileSetup(installPath, options = {}) {
|
||||
try {
|
||||
const dashboardPath = path.join(installPath, 'sites', 'status');
|
||||
const caddyfilePath = path.join(installPath, 'Caddyfile');
|
||||
|
||||
let content;
|
||||
if (options.domainMode === 'public') {
|
||||
content = this.generateCaddyfileWithDomain({
|
||||
dashboardPath,
|
||||
domain: options.publicDomain,
|
||||
email: options.email,
|
||||
tier: options.tier,
|
||||
apiPort: options.apiPort
|
||||
});
|
||||
} else if (options.domainMode === 'custom-tld') {
|
||||
content = this.generateCaddyfileCustomTLD({
|
||||
dashboardPath,
|
||||
installPath,
|
||||
tld: options.tld,
|
||||
caName: options.caName || 'DashCaddy Local CA',
|
||||
tier: options.tier,
|
||||
apiPort: options.apiPort
|
||||
});
|
||||
} else {
|
||||
content = this.generateCaddyfile({
|
||||
dashboardPath,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.writeCaddyfile(content, caddyfilePath);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: caddyfilePath,
|
||||
content
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate docker-compose.yml for running the API server
|
||||
* @param {string} installPath - Installation directory
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {number} options.apiPort - API server port
|
||||
* @param {string} options.lanIP - Host LAN IP address
|
||||
* @param {string} options.tailscaleIP - Host Tailscale IP address
|
||||
* @param {string} options.domainMode - Domain mode (local, public, custom-tld)
|
||||
*/
|
||||
generateDockerCompose(installPath, options = {}) {
|
||||
const apiPort = options.apiPort || DEFAULT_PORTS.API;
|
||||
const adminPort = DEFAULT_PORTS.CADDY_ADMIN;
|
||||
const p = this._p.bind(this);
|
||||
|
||||
// Core volume mounts
|
||||
let volumes = ` - ${p(installPath)}/Caddyfile:/caddyfile:rw
|
||||
- ${p(installPath)}/services.json:/app/services.json:rw
|
||||
- ${p(installPath)}/dns-credentials.json:/app/dns-credentials.json:rw
|
||||
- ${p(installPath)}/config.json:/app/config.json:rw
|
||||
- ${p(installPath)}/credentials.json:/app/credentials.json:rw
|
||||
- ${p(installPath)}/.encryption-key:/app/.encryption-key:rw
|
||||
- ${p(installPath)}/notifications.json:/app/notifications.json:rw
|
||||
- ${p(installPath)}/sites/status/assets:/app/assets:rw
|
||||
- /var/run/docker.sock:/var/run/docker.sock`;
|
||||
|
||||
// Add CA cert mount for custom-tld mode
|
||||
if (options.domainMode === 'custom-tld') {
|
||||
volumes += `\n - ${p(installPath)}/certs/pki/authorities/local:/app/pki:ro`;
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
let envVars = ` - CADDYFILE_PATH=/caddyfile
|
||||
- CADDY_ADMIN_URL=http://host.docker.internal:${adminPort}
|
||||
- ASSETS_PATH=/app/assets
|
||||
- CREDENTIALS_FILE=/app/credentials.json
|
||||
- NODE_ENV=production`;
|
||||
|
||||
if (options.domainMode === 'custom-tld') {
|
||||
envVars += `\n - CA_CERT_PATH=/app/pki/root.crt`;
|
||||
}
|
||||
|
||||
if (options.lanIP) {
|
||||
envVars += `\n - HOST_LAN_IP=${options.lanIP}`;
|
||||
}
|
||||
if (options.tailscaleIP) {
|
||||
envVars += `\n - HOST_TAILSCALE_IP=${options.tailscaleIP}`;
|
||||
}
|
||||
|
||||
const dockerCompose = `services:
|
||||
dashcaddy-api:
|
||||
build: .
|
||||
container_name: dashcaddy-api
|
||||
ports:
|
||||
- "${apiPort}:${apiPort}"
|
||||
volumes:
|
||||
${volumes}
|
||||
environment:
|
||||
${envVars}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
`;
|
||||
|
||||
return dockerCompose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write docker-compose.yml to disk
|
||||
* @param {string} installPath - Installation directory
|
||||
* @param {Object} options - Configuration options
|
||||
*/
|
||||
async createDockerCompose(installPath, options = {}) {
|
||||
try {
|
||||
const content = this.generateDockerCompose(installPath, options);
|
||||
const apiDir = path.join(installPath, 'sites', 'dashcaddy-api');
|
||||
await fs.promises.mkdir(apiDir, { recursive: true });
|
||||
const outputPath = path.join(apiDir, 'docker-compose.yml');
|
||||
|
||||
await fs.promises.writeFile(outputPath, content, 'utf8');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: outputPath,
|
||||
content
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CaddyfileGenerator;
|
||||
Reference in New Issue
Block a user