Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37a3282f98 | ||
|
|
1fbe65f524 | ||
|
|
320f21c113 | ||
|
|
5c76c3df97 | ||
|
|
260575c6bd |
@@ -10,7 +10,26 @@ const lockfile = require('proper-lockfile');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CREDENTIALS_FILE = process.env.CREDENTIALS_FILE || path.join(__dirname, 'credentials.json');
|
||||
// Resolve credentials file path — supports both standard install (/app/credentials.json)
|
||||
// and custom deployments with consolidated data directory (/app/data/credentials.json)
|
||||
function resolveCredentialsFile() {
|
||||
if (process.env.CREDENTIALS_FILE) {
|
||||
return process.env.CREDENTIALS_FILE;
|
||||
}
|
||||
const candidates = [
|
||||
path.join(__dirname, 'credentials.json'),
|
||||
path.join(__dirname, 'data', 'credentials.json'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
// No existing file — return standard path so first store() creates it there
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
const CREDENTIALS_FILE = resolveCredentialsFile();
|
||||
|
||||
class CredentialManager {
|
||||
constructor() {
|
||||
|
||||
@@ -15,8 +15,26 @@ const IV_LENGTH = 16; // 128 bits for GCM
|
||||
const AUTH_TAG_LENGTH = 16;
|
||||
const SALT_LENGTH = 32;
|
||||
|
||||
// Key file location (should be outside of mounted volumes for security)
|
||||
const KEY_FILE = process.env.ENCRYPTION_KEY_FILE || path.join(__dirname, '.encryption-key');
|
||||
// Resolve encryption key file path — supports both standard install (/app/.encryption-key)
|
||||
// and custom deployments with consolidated data directory (/app/data/.encryption-key)
|
||||
function resolveKeyFile() {
|
||||
if (process.env.ENCRYPTION_KEY_FILE) {
|
||||
return process.env.ENCRYPTION_KEY_FILE;
|
||||
}
|
||||
const candidates = [
|
||||
path.join(__dirname, '.encryption-key'),
|
||||
path.join(__dirname, 'data', '.encryption-key'),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
// No existing file — return standard path so first load creates it there
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
const KEY_FILE = resolveKeyFile();
|
||||
|
||||
let encryptionKey = null;
|
||||
|
||||
|
||||
@@ -305,6 +305,8 @@ module.exports = function configureMiddleware(app, {
|
||||
{ path: '/api/v1/config', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/services/status', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/system/update-notify', exact: true, method: 'POST' },
|
||||
{ path: '/api/v1/monitoring/stats', exact: true, method: 'GET' },
|
||||
{ path: '/api/v1/health-checks/status', exact: true, method: 'GET' },
|
||||
];
|
||||
|
||||
function isPublicRoute(req) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dashcaddy-api",
|
||||
"version": "1.11.0",
|
||||
"version": "1.12.0",
|
||||
"description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// All paths can be overridden via environment variables.
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Base directories
|
||||
@@ -34,6 +35,8 @@ const paths = {
|
||||
caCertDir: path.join(CADDY_SITES, 'ca'),
|
||||
pkiRootCert: path.join(CADDY_PKI, 'root.crt'),
|
||||
pkiIntermediateCert: path.join(CADDY_PKI, 'intermediate.crt'),
|
||||
generatedCertsDir: path.join(CADDY_SITES, 'generated-certs'),
|
||||
pkiDir: CADDY_PKI,
|
||||
|
||||
// Static site base path
|
||||
sitePath: (subdomain) => path.join(CADDY_SITES, subdomain),
|
||||
@@ -41,6 +44,24 @@ const paths = {
|
||||
// Docker data path for app volumes
|
||||
appData: (appName) => path.join(DOCKER_DATA, appName),
|
||||
|
||||
// In-container paths (used by self-updater and Docker deployments)
|
||||
// Override via env vars for custom Docker layouts
|
||||
containerUpdatesDir: process.env.DASHCADDY_UPDATES_DIR || '/app/updates',
|
||||
containerFrontendDir: process.env.DASHCADDY_FRONTEND_DIR || '/app/dashboard',
|
||||
containerAssetsDir: process.env.ASSETS_DIR || '/app/assets',
|
||||
|
||||
// Asset path resolution — supports both Docker (single file mount) and
|
||||
// consolidated data directory layouts
|
||||
resolveAssetsPath: (envPath) => {
|
||||
if (envPath) return envPath;
|
||||
// Standard Docker mount: /app/assets (volume-mounted)
|
||||
if (fs.existsSync('/app/assets')) return '/app/assets';
|
||||
// Consolidated data directory: /app/data/assets
|
||||
if (fs.existsSync(path.join(CADDY_BASE, 'assets'))) return path.join(CADDY_BASE, 'assets');
|
||||
// Fall back to /app/assets even if it doesn't exist (will create on write)
|
||||
return '/app/assets';
|
||||
},
|
||||
|
||||
// Log digest directory
|
||||
digestDir: process.env.DIGEST_DIR || path.join(CADDY_BASE, 'digests'),
|
||||
|
||||
|
||||
@@ -226,7 +226,23 @@ const server = http.createServer(async (req, res) => {
|
||||
json(res, 404, { error: 'Not found' });
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[Pylon] ${PYLON_NAME} listening on port ${PORT}`);
|
||||
const PYLON_PORT = parseInt(process.env.PYLON_PORT, 10) || 7842;
|
||||
const PYLON_HOST = process.env.PYLON_HOST || '0.0.0.0';
|
||||
|
||||
server.listen(PYLON_PORT, PYLON_HOST, () => {
|
||||
console.log(`[Pylon] ${PYLON_NAME} listening on ${PYLON_HOST}:${PYLON_PORT}`);
|
||||
if (API_KEY) console.log('[Pylon] API key authentication enabled');
|
||||
});
|
||||
|
||||
// Graceful shutdown — drain connections, then exit
|
||||
const shutdown = (signal) => {
|
||||
console.log(`[Pylon] ${signal} received, draining...`);
|
||||
server.close(() => {
|
||||
console.log('[Pylon] HTTP server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
// Force exit after 5s if connections don't drain
|
||||
setTimeout(() => process.exit(0), 5000).unref();
|
||||
};
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
|
||||
@@ -197,8 +197,18 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
|
||||
}
|
||||
}
|
||||
|
||||
const container = await docker.client.createContainer(containerConfig);
|
||||
await container.start();
|
||||
let container;
|
||||
try {
|
||||
container = await docker.client.createContainer(containerConfig);
|
||||
await container.start();
|
||||
} catch (createErr) {
|
||||
// If create fails with "no such image", wrap with user-friendly message
|
||||
const errMsg = createErr?.message || String(createErr);
|
||||
if (errMsg.includes('No such image') || errMsg.includes('no such image')) {
|
||||
throw new Error(`[DC-201] Image pull succeeded but container creation failed — image may be corrupted: ${processedTemplate.docker.image}. ${errMsg}`);
|
||||
}
|
||||
throw createErr;
|
||||
}
|
||||
|
||||
// Prune dangling images to prevent disk bloat
|
||||
try {
|
||||
|
||||
+10
-16
@@ -12,14 +12,11 @@ module.exports = function(ctx) {
|
||||
|
||||
// Get CA certificate information
|
||||
router.get('/info', ctx.asyncHandler(async (req, res) => {
|
||||
const certInfoPath = '/app/ca/cert-info.json';
|
||||
const fallbackCertInfoPath = path.join(platformPaths.caCertDir, 'cert-info.json');
|
||||
const certInfoPath = path.join(platformPaths.caCertDir, 'cert-info.json');
|
||||
|
||||
let certInfoFile;
|
||||
if (await exists(certInfoPath)) {
|
||||
certInfoFile = certInfoPath;
|
||||
} else if (await exists(fallbackCertInfoPath)) {
|
||||
certInfoFile = fallbackCertInfoPath;
|
||||
} else {
|
||||
const { NotFoundError } = require('../errors');
|
||||
throw new NotFoundError('CA certificate information');
|
||||
@@ -46,13 +43,11 @@ module.exports = function(ctx) {
|
||||
|
||||
// Serve root CA certificate directly (works even without DashCA deployed)
|
||||
router.get('/root.crt', ctx.asyncHandler(async (req, res) => {
|
||||
const pkiCertPath = '/app/pki/root.crt';
|
||||
const hostCertPath = platformPaths.pkiRootCert;
|
||||
const dashcaCertPath = path.join(platformPaths.caCertDir, 'root.crt');
|
||||
|
||||
let certPath;
|
||||
if (await exists(pkiCertPath)) certPath = pkiCertPath;
|
||||
else if (await exists(dashcaCertPath)) certPath = dashcaCertPath;
|
||||
if (await exists(dashcaCertPath)) certPath = dashcaCertPath;
|
||||
else if (await exists(hostCertPath)) certPath = hostCertPath;
|
||||
else {
|
||||
const { NotFoundError } = require('../errors');
|
||||
@@ -72,13 +67,12 @@ module.exports = function(ctx) {
|
||||
}
|
||||
|
||||
// Load cert info to get the fingerprint
|
||||
const certInfoPath = '/app/ca/cert-info.json';
|
||||
const fallbackCertInfoPath2 = path.join(platformPaths.caCertDir, 'cert-info.json');
|
||||
const certInfoPath = path.join(platformPaths.caCertDir, 'cert-info.json');
|
||||
|
||||
let certInfoFile;
|
||||
if (await exists(certInfoPath)) certInfoFile = certInfoPath;
|
||||
else if (await exists(fallbackCertInfoPath2)) certInfoFile = fallbackCertInfoPath2;
|
||||
else {
|
||||
if (await exists(certInfoPath)) {
|
||||
certInfoFile = certInfoPath;
|
||||
} else {
|
||||
const { NotFoundError } = require('../errors');
|
||||
throw new NotFoundError('CA certificate information. Deploy DashCA first or ensure cert-info.json exists.');
|
||||
}
|
||||
@@ -100,7 +94,7 @@ module.exports = function(ctx) {
|
||||
// Look for template in multiple locations (packaged app vs dev)
|
||||
const templatePaths = [
|
||||
path.join(__dirname, '..', 'scripts', templateName),
|
||||
path.join('/app', 'scripts', templateName)
|
||||
path.join(platformPaths.caddyBase, 'scripts', templateName)
|
||||
];
|
||||
|
||||
let templateContent;
|
||||
@@ -142,8 +136,8 @@ module.exports = function(ctx) {
|
||||
return ctx.errorResponse(res, 400, `Invalid domain name. Must be a valid hostname (e.g., dns1${ctx.siteConfig.tld})`);
|
||||
}
|
||||
|
||||
const pkiPath = '/app/pki';
|
||||
const certsDir = '/app/generated-certs';
|
||||
const pkiPath = platformPaths.pkiDir;
|
||||
const certsDir = platformPaths.generatedCertsDir;
|
||||
const domainDir = path.join(certsDir, domain);
|
||||
|
||||
const intermediateCert = path.join(pkiPath, 'intermediate.crt');
|
||||
@@ -246,7 +240,7 @@ ${safeDomain.includes('.') ? `DNS.2 = *.${safeDomain}` : ''}`;
|
||||
|
||||
// List generated certificates
|
||||
router.get('/certs', ctx.asyncHandler(async (req, res) => {
|
||||
const certsDir = '/app/generated-certs';
|
||||
const certsDir = platformPaths.generatedCertsDir;
|
||||
|
||||
if (!await exists(certsDir)) {
|
||||
return res.json({ success: true, certificates: [] });
|
||||
|
||||
@@ -4,6 +4,7 @@ const path = require('path');
|
||||
const { LIMITS } = require('../../constants');
|
||||
const { exists } = require('../../fs-helpers');
|
||||
const { ValidationError } = require('../../errors');
|
||||
const platformPaths = require('../../platform-paths');
|
||||
/**
|
||||
* Config assets routes factory
|
||||
* @param {Object} deps - Explicit dependencies
|
||||
@@ -51,7 +52,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
// Determine assets path (mounted volume)
|
||||
const assetsPath = process.env.ASSETS_PATH || '/app/assets';
|
||||
const assetsPath = platformPaths.resolveAssetsPath(process.env.ASSETS_PATH);
|
||||
|
||||
// Ensure directory exists
|
||||
if (!await exists(assetsPath)) {
|
||||
@@ -96,7 +97,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
||||
const extension = matches[1] === 'svg+xml' ? 'svg' : matches[1];
|
||||
const buffer = Buffer.from(matches[2], 'base64');
|
||||
|
||||
const assetsPath = process.env.ASSETS_PATH || '/app/assets';
|
||||
const assetsPath = platformPaths.resolveAssetsPath(process.env.ASSETS_PATH);
|
||||
if (!await exists(assetsPath)) {
|
||||
await fsp.mkdir(assetsPath, { recursive: true });
|
||||
}
|
||||
@@ -170,7 +171,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
||||
// Reset all branding to defaults
|
||||
router.delete('/logo', asyncHandler(async (req, res) => {
|
||||
const config = await ctx.readConfig();
|
||||
const assetsPath = process.env.ASSETS_PATH || '/app/assets';
|
||||
const assetsPath = platformPaths.resolveAssetsPath(process.env.ASSETS_PATH);
|
||||
|
||||
// Delete all custom logo files
|
||||
const logoPaths = [config.customLogo, config.customLogoDark, config.customLogoLight].filter(Boolean);
|
||||
@@ -234,7 +235,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
||||
const base64Data = matches[2];
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
|
||||
const assetsPath = process.env.ASSETS_PATH || '/app/assets';
|
||||
const assetsPath = platformPaths.resolveAssetsPath(process.env.ASSETS_PATH);
|
||||
if (!await exists(assetsPath)) {
|
||||
await fsp.mkdir(assetsPath, { recursive: true });
|
||||
}
|
||||
@@ -279,7 +280,7 @@ module.exports = function({ servicesStateManager: _servicesStateManager, asyncHa
|
||||
const config = await ctx.readConfig();
|
||||
|
||||
// Delete custom favicon files
|
||||
const assetsPath = process.env.ASSETS_PATH || '/app/assets';
|
||||
const assetsPath = platformPaths.resolveAssetsPath(process.env.ASSETS_PATH);
|
||||
const filesToDelete = ['favicon.ico', 'favicon.png'];
|
||||
for (const file of filesToDelete) {
|
||||
const filePath = `${assetsPath}/${file}`;
|
||||
|
||||
@@ -4,6 +4,7 @@ const path = require('path');
|
||||
const { CADDY } = require('../../constants');
|
||||
const { exists } = require('../../fs-helpers');
|
||||
const { ValidationError, AuthenticationError } = require('../../errors');
|
||||
const platformPaths = require('../../platform-paths');
|
||||
|
||||
/**
|
||||
* Config backup routes factory
|
||||
@@ -115,7 +116,7 @@ module.exports = function(deps) {
|
||||
|
||||
// Include custom assets (logo, favicon) as base64
|
||||
try {
|
||||
const assetsDir = process.env.ASSETS_DIR || '/app/assets';
|
||||
const assetsDir = platformPaths.resolveAssetsPath(process.env.ASSETS_DIR);
|
||||
const configData = backup.files.config?.data || {};
|
||||
const assetFiles = [configData.customLogo, configData.customFavicon]
|
||||
.filter(Boolean)
|
||||
@@ -346,7 +347,7 @@ module.exports = function(deps) {
|
||||
|
||||
// Restore custom assets from base64
|
||||
if (backup.assets && typeof backup.assets === 'object') {
|
||||
const assetsDir = process.env.ASSETS_DIR || '/app/assets';
|
||||
const assetsDir = platformPaths.resolveAssetsPath(process.env.ASSETS_DIR);
|
||||
for (const [name, b64] of Object.entries(backup.assets)) {
|
||||
try {
|
||||
const safeName = path.basename(name); // prevent path traversal
|
||||
|
||||
@@ -322,9 +322,16 @@ module.exports = function({
|
||||
// ===== HEALTH CHECK (health-checker module) =====
|
||||
|
||||
// Get current status for all services
|
||||
// Returns per-service status plus a summary for the System Overview widget:
|
||||
// { status: { ... }, summary: { healthy, unhealthy, total } }
|
||||
router.get('/health-checks/status', asyncHandler(async (req, res) => {
|
||||
const status = healthChecker.getCurrentStatus();
|
||||
success(res, { status });
|
||||
// Build summary for the overview widget
|
||||
const entries = Object.values(status);
|
||||
const healthy = entries.filter(s => s.status === 'up' || s.status === 'healthy').length;
|
||||
const unhealthy = entries.filter(s => s.status === 'down' || s.status === 'unhealthy').length;
|
||||
const total = entries.length;
|
||||
success(res, { status, summary: { healthy, unhealthy, total } });
|
||||
}, 'health-check-status'));
|
||||
|
||||
// Get service statistics
|
||||
|
||||
@@ -16,8 +16,22 @@ module.exports = function({ resourceMonitor, docker, asyncHandler, log, notifica
|
||||
// ===== RESOURCE MONITORING ENDPOINTS =====
|
||||
|
||||
// Get all container stats (from resource monitor module)
|
||||
// Returns a flat summary format for the System Overview widget:
|
||||
// { containerId: { cpu: <percent>, memory: <percent>, memoryUsage: <bytes>, name } }
|
||||
router.get('/monitoring/stats', asyncHandler(async (req, res) => {
|
||||
const stats = resourceMonitor.getAllStats();
|
||||
const raw = resourceMonitor.getAllStats();
|
||||
// Transform nested { current: { cpu: { percent }, memory: { percent, usage } } }
|
||||
// into flat { cpu: number, memory: number, memoryUsage: number } for the frontend widget
|
||||
const stats = {};
|
||||
for (const [id, data] of Object.entries(raw)) {
|
||||
const cur = data.current || {};
|
||||
stats[id] = {
|
||||
name: data.name,
|
||||
cpu: typeof cur.cpu === 'object' ? (cur.cpu.percent ?? 0) : (Number(cur.cpu) || 0),
|
||||
memory: typeof cur.memory === 'object' ? (cur.memory.percent ?? 0) : (Number(cur.memory) || 0),
|
||||
memoryUsage: typeof cur.memory === 'object' ? (cur.memory.usage ?? 0) : 0,
|
||||
};
|
||||
}
|
||||
success(res, { stats });
|
||||
}, 'monitoring-stats'));
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ module.exports = function({
|
||||
dns
|
||||
}) {
|
||||
const router = express.Router();
|
||||
const CA_CERT_PATH = process.env.CA_CERT_PATH || '/app/pki/root.crt';
|
||||
const CA_CERT_PATH = process.env.CA_CERT_PATH || platformPaths.pkiRootCert;
|
||||
const PROBE_CONCURRENCY = 6;
|
||||
let probeHttpsAgent;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { success } = require('../response-helpers');
|
||||
const { ValidationError, NotFoundError } = require('../errors');
|
||||
const platformPaths = require('../platform-paths');
|
||||
|
||||
/**
|
||||
* Themes routes factory
|
||||
@@ -13,7 +14,7 @@ const { ValidationError, NotFoundError } = require('../errors');
|
||||
*/
|
||||
module.exports = function({ asyncHandler, log }) {
|
||||
const router = express.Router();
|
||||
const THEMES_DIR = process.env.THEMES_DIR || path.join(path.dirname(process.env.SERVICES_FILE || '/app/services.json'), 'themes');
|
||||
const THEMES_DIR = process.env.THEMES_DIR || path.join(path.dirname(platformPaths.servicesFile), 'themes');
|
||||
|
||||
// Ensure themes directory exists
|
||||
if (!fs.existsSync(THEMES_DIR)) {
|
||||
|
||||
@@ -21,17 +21,17 @@ const isWindows = platformPaths.isWindows;
|
||||
|
||||
const DEFAULTS = {
|
||||
CHECK_INTERVAL: 30 * 60 * 1000, // 30 minutes
|
||||
UPDATE_URL: 'https://get.dashcaddy.net/release',
|
||||
MIRROR_URL: 'https://get2.dashcaddy.net/release',
|
||||
UPDATES_DIR: platformPaths.isWindows ? path.join(platformPaths.caddyBase, 'updates') : '/app/updates',
|
||||
UPDATE_URL: process.env.DASHCADDY_UPDATE_URL || 'https://get.dashcaddy.net/release',
|
||||
MIRROR_URL: process.env.DASHCADDY_MIRROR_URL || 'https://get2.dashcaddy.net/release',
|
||||
UPDATES_DIR: platformPaths.containerUpdatesDir,
|
||||
// API_SOURCE_DIR is the HOST path — written to trigger.json for the host-side updater
|
||||
API_SOURCE_DIR: path.join(platformPaths.caddySites, 'dashcaddy-api'),
|
||||
// FRONTEND_DIR is the container path — dashboard is volume-mounted at /app/dashboard
|
||||
FRONTEND_DIR: platformPaths.isWindows ? path.join(platformPaths.caddySites, 'status') : '/app/dashboard',
|
||||
FRONTEND_DIR: platformPaths.containerFrontendDir,
|
||||
MAX_BACKUPS: 3,
|
||||
HEALTH_TIMEOUT: 60000,
|
||||
DOWNLOAD_TIMEOUT: 120000,
|
||||
CHANNEL: 'stable',
|
||||
CHANNEL: process.env.DASHCADDY_UPDATE_CHANNEL || 'stable',
|
||||
INSTANCE_ID_FILE: platformPaths.isWindows
|
||||
? path.join(platformPaths.caddyBase, 'instance-id')
|
||||
: '/etc/dashcaddy/instance-id',
|
||||
|
||||
@@ -25,7 +25,8 @@ process.on('uncaughtException', (error) => {
|
||||
// Load license
|
||||
await licenseManager.load();
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const PORT = parseInt(process.env.PORT, 10) || 3001;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
const CADDYFILE_PATH = process.env.CADDYFILE_PATH || platformPaths.caddyfile;
|
||||
const CADDY_ADMIN_URL = process.env.CADDY_ADMIN_URL || platformPaths.caddyAdminUrl;
|
||||
const SERVICES_FILE = process.env.SERVICES_FILE || platformPaths.servicesFile;
|
||||
@@ -43,9 +44,10 @@ process.on('uncaughtException', (error) => {
|
||||
});
|
||||
|
||||
// Start HTTP server
|
||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
||||
const server = app.listen(PORT, HOST, () => {
|
||||
log.info('server', 'DashCaddy API server started', {
|
||||
port: PORT,
|
||||
host: HOST,
|
||||
caddyfile: CADDYFILE_PATH,
|
||||
caddyAdmin: CADDY_ADMIN_URL,
|
||||
services: SERVICES_FILE,
|
||||
|
||||
@@ -16,6 +16,7 @@ const { asyncHandler } = require('./utils/async-handler');
|
||||
|
||||
// Managers and utilities
|
||||
const StateManager = require('../state-manager');
|
||||
const platformPaths = require('../platform-paths');
|
||||
const { LicenseManager } = require('../license-manager');
|
||||
const credentialManager = require('../credential-manager');
|
||||
const authManager = require('../auth-manager');
|
||||
@@ -96,6 +97,19 @@ const { APP } = require('../constants');
|
||||
async function createApp() {
|
||||
const app = express();
|
||||
|
||||
// Global request timeout (default 5 minutes — covers slow Docker pulls)
|
||||
// Routes that need longer can override per-request with req.setTimeout()
|
||||
const REQUEST_TIMEOUT_MS = parseInt(process.env.REQUEST_TIMEOUT_MS, 10) || 5 * 60 * 1000;
|
||||
app.use((req, res, next) => {
|
||||
req.setTimeout(REQUEST_TIMEOUT_MS);
|
||||
res.setTimeout(REQUEST_TIMEOUT_MS);
|
||||
next();
|
||||
});
|
||||
// Disable x-powered-by header for security (don't advertise framework)
|
||||
app.disable('x-powered-by');
|
||||
// Trust first proxy (Caddy/nginx in front of us) so req.ip works correctly
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// Initialize logging
|
||||
const log = createLogger(config.LOG_LEVEL);
|
||||
|
||||
@@ -111,7 +125,7 @@ async function createApp() {
|
||||
licenseManager.loadSecret(config.LICENSE_SECRET_FILE);
|
||||
|
||||
// HTTPS agent for internal CA
|
||||
const CA_CERT_PATH = process.env.CA_CERT_PATH || '/app/pki/root.crt';
|
||||
const CA_CERT_PATH = process.env.CA_CERT_PATH || platformPaths.pkiRootCert;
|
||||
let httpsAgent;
|
||||
try {
|
||||
const caCert = fs.readFileSync(CA_CERT_PATH);
|
||||
@@ -380,6 +394,29 @@ async function createApp() {
|
||||
// Build versioned API router
|
||||
const apiRouter = express.Router();
|
||||
|
||||
// Version endpoint — public, no auth required
|
||||
// Reads version from package.json at startup so the response always matches the running code
|
||||
let appVersion = '0.0.0';
|
||||
let appName = 'dashcaddy-api';
|
||||
try {
|
||||
const pkg = require('../package.json');
|
||||
appVersion = pkg.version || appVersion;
|
||||
appName = pkg.name || appName;
|
||||
} catch { /* package.json unreadable — keep fallback */ }
|
||||
apiRouter.get('/version', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
name: appName,
|
||||
version: appVersion,
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
uptime: process.uptime(),
|
||||
instanceId: process.env.DASHCADDY_INSTANCE_ID || null
|
||||
});
|
||||
});
|
||||
log.info('app', `Version endpoint available at /api/v1/version (v${appVersion})`);
|
||||
|
||||
// Wire up notification listeners for resourceMonitor and backupManager
|
||||
if (ctx.notification && ctx.resourceMonitor) {
|
||||
ctx.resourceMonitor.on('alert', (alertData) => {
|
||||
|
||||
Reference in New Issue
Block a user