Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d394d882d |
@@ -538,7 +538,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('healthy');
|
expect(res.body.caStatus).toBe('healthy');
|
||||||
expect(res.body.daysUntilExpiration).toBeGreaterThan(90);
|
expect(res.body.daysUntilExpiration).toBeGreaterThan(90);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -551,7 +551,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('warning');
|
expect(res.body.caStatus).toBe('warning');
|
||||||
expect(res.body.daysUntilExpiration).toBeLessThan(90);
|
expect(res.body.daysUntilExpiration).toBeLessThan(90);
|
||||||
expect(res.body.daysUntilExpiration).toBeGreaterThanOrEqual(30);
|
expect(res.body.daysUntilExpiration).toBeGreaterThanOrEqual(30);
|
||||||
});
|
});
|
||||||
@@ -565,7 +565,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('critical');
|
expect(res.body.caStatus).toBe('critical');
|
||||||
expect(res.body.daysUntilExpiration).toBeLessThan(30);
|
expect(res.body.daysUntilExpiration).toBeLessThan(30);
|
||||||
expect(res.body.daysUntilExpiration).toBeGreaterThanOrEqual(0);
|
expect(res.body.daysUntilExpiration).toBeGreaterThanOrEqual(0);
|
||||||
});
|
});
|
||||||
@@ -579,7 +579,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('critical');
|
expect(res.body.caStatus).toBe('critical');
|
||||||
expect(res.body.daysUntilExpiration).toBeLessThan(7);
|
expect(res.body.daysUntilExpiration).toBeLessThan(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -592,7 +592,7 @@ describe('Health Routes', () => {
|
|||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.status).toBe('critical');
|
expect(res.body.caStatus).toBe('critical');
|
||||||
expect(res.body.daysUntilExpiration).toBeLessThan(0);
|
expect(res.body.daysUntilExpiration).toBeLessThan(0);
|
||||||
expect(res.body.message).toMatch(/EXPIRED/);
|
expect(res.body.message).toMatch(/EXPIRED/);
|
||||||
});
|
});
|
||||||
@@ -601,9 +601,9 @@ describe('Health Routes', () => {
|
|||||||
exists.mockResolvedValue(false);
|
exists.mockResolvedValue(false);
|
||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(404);
|
||||||
expect(res.body.status).toBe('error');
|
expect(res.body.caStatus).toBe('error');
|
||||||
expect(res.body.message).toMatch(/not found/);
|
expect(res.body.error).toMatch(/not found/);
|
||||||
expect(res.body.daysUntilExpiration).toBeNull();
|
expect(res.body.daysUntilExpiration).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -612,9 +612,9 @@ describe('Health Routes', () => {
|
|||||||
execSync.mockImplementation(() => { throw new Error('openssl not found'); });
|
execSync.mockImplementation(() => { throw new Error('openssl not found'); });
|
||||||
const { app } = createApp();
|
const { app } = createApp();
|
||||||
const res = await request(app).get('/api/health/ca');
|
const res = await request(app).get('/api/health/ca');
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(500);
|
||||||
expect(res.body.status).toBe('error');
|
expect(res.body.caStatus).toBe('error');
|
||||||
expect(res.body.message).toBe('openssl not found');
|
expect(res.body.error).toBe('openssl not found');
|
||||||
expect(res.body.daysUntilExpiration).toBeNull();
|
expect(res.body.daysUntilExpiration).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dashcaddy-api",
|
"name": "dashcaddy-api",
|
||||||
"version": "1.13.2",
|
"version": "1.13.3",
|
||||||
"description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management",
|
"description": "DashCaddy API server - Dashboard backend for Docker, Caddy & DNS management",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -383,9 +383,8 @@ module.exports = function({
|
|||||||
|
|
||||||
const response = await fetchT(technitiumUrl, {
|
const response = await fetchT(technitiumUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Accept': 'text/plain' },
|
headers: { 'Accept': 'text/plain' }
|
||||||
timeout: 10000
|
}, 10000);
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
@@ -640,7 +639,7 @@ module.exports = function({
|
|||||||
const dnsPort = siteConfig.dnsServerPort || '5380';
|
const dnsPort = siteConfig.dnsServerPort || '5380';
|
||||||
try {
|
try {
|
||||||
const url = `http://${serverInfo.ip}:${dnsPort}/api/admin/restart?token=${encodeURIComponent(tokenResult.token)}`;
|
const url = `http://${serverInfo.ip}:${dnsPort}/api/admin/restart?token=${encodeURIComponent(tokenResult.token)}`;
|
||||||
const response = await fetchT(url, { method: 'POST', timeout: 5000 });
|
const response = await fetchT(url, { method: 'POST' }, 5000);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.status === 'ok') {
|
if (result.status === 'ok') {
|
||||||
success(res, { message: 'Restart initiated' });
|
success(res, { message: 'Restart initiated' });
|
||||||
|
|||||||
@@ -273,9 +273,10 @@ module.exports = function({
|
|||||||
try {
|
try {
|
||||||
// Check if certificate exists
|
// Check if certificate exists
|
||||||
if (!await exists(rootCertPath)) {
|
if (!await exists(rootCertPath)) {
|
||||||
return res.json({
|
return res.status(404).json({
|
||||||
status: 'error',
|
success: false,
|
||||||
message: 'Root CA certificate not found',
|
error: 'Root CA certificate not found',
|
||||||
|
caStatus: 'error',
|
||||||
daysUntilExpiration: null
|
daysUntilExpiration: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -286,34 +287,36 @@ module.exports = function({
|
|||||||
const daysUntilExpiration = Math.floor((expirationDate - new Date()) / (1000 * 60 * 60 * 24));
|
const daysUntilExpiration = Math.floor((expirationDate - new Date()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
// Alert thresholds
|
// Alert thresholds
|
||||||
let status = 'healthy';
|
let caStatus = 'healthy';
|
||||||
let message = `CA certificate valid for ${daysUntilExpiration} days`;
|
let message = `CA certificate valid for ${daysUntilExpiration} days`;
|
||||||
|
|
||||||
if (daysUntilExpiration < 0) {
|
if (daysUntilExpiration < 0) {
|
||||||
status = 'critical';
|
caStatus = 'critical';
|
||||||
message = `CA certificate EXPIRED ${Math.abs(daysUntilExpiration)} days ago!`;
|
message = `CA certificate EXPIRED ${Math.abs(daysUntilExpiration)} days ago!`;
|
||||||
} else if (daysUntilExpiration < 7) {
|
} else if (daysUntilExpiration < 7) {
|
||||||
status = 'critical';
|
caStatus = 'critical';
|
||||||
message = `CA certificate expires in ${daysUntilExpiration} days!`;
|
message = `CA certificate expires in ${daysUntilExpiration} days!`;
|
||||||
} else if (daysUntilExpiration < 30) {
|
} else if (daysUntilExpiration < 30) {
|
||||||
status = 'critical';
|
caStatus = 'critical';
|
||||||
message = `CA certificate expires in ${daysUntilExpiration} days!`;
|
message = `CA certificate expires in ${daysUntilExpiration} days!`;
|
||||||
} else if (daysUntilExpiration < 90) {
|
} else if (daysUntilExpiration < 90) {
|
||||||
status = 'warning';
|
caStatus = 'warning';
|
||||||
message = `CA certificate expires in ${daysUntilExpiration} days`;
|
message = `CA certificate expires in ${daysUntilExpiration} days`;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
status: status,
|
success: true,
|
||||||
message: message,
|
caStatus,
|
||||||
daysUntilExpiration: daysUntilExpiration,
|
message,
|
||||||
|
daysUntilExpiration,
|
||||||
expiresAt: notAfter
|
expiresAt: notAfter
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await logError('GET /api/health/ca', error);
|
await logError('GET /api/health/ca', error);
|
||||||
res.json({
|
res.status(500).json({
|
||||||
status: 'error',
|
success: false,
|
||||||
message: error.message,
|
error: error.message,
|
||||||
|
caStatus: 'error',
|
||||||
daysUntilExpiration: null
|
daysUntilExpiration: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|||||||
const { CADDY, REGEX, LIMITS } = require('../constants');
|
const { CADDY, REGEX, LIMITS } = require('../constants');
|
||||||
const { ValidationError, ConflictError, NotFoundError } = require('../errors');
|
const { ValidationError, ConflictError, NotFoundError } = require('../errors');
|
||||||
const { validateURL } = require('../input-validator');
|
const { validateURL } = require('../input-validator');
|
||||||
|
const { ok } = require('../src/utils/responses');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sites route factory
|
* Sites route factory
|
||||||
@@ -127,7 +128,7 @@ module.exports = function({ asyncHandler, caddy, dns, fetchT, buildDomain, addSe
|
|||||||
name: ca.name,
|
name: ca.name,
|
||||||
displayName: ca.name !== (ca.id || ca.name) ? `${ca.name} (${ca.id || ca.name})` : ca.name
|
displayName: ca.name !== (ca.id || ca.name) ? `${ca.name} (${ca.id || ca.name})` : ca.name
|
||||||
}));
|
}));
|
||||||
res.json({ status: 'success', data: { cas: caList } });
|
ok(res, { cas: caList });
|
||||||
}, 'caddy-get-cas'));
|
}, 'caddy-get-cas'));
|
||||||
|
|
||||||
// Remove a site from Caddyfile
|
// Remove a site from Caddyfile
|
||||||
|
|||||||
@@ -93,9 +93,8 @@ async function verifySiteAccessible(domain, fetchT, httpsAgent, log, maxAttempts
|
|||||||
try {
|
try {
|
||||||
const response = await fetchT(`https://${domain}/`, {
|
const response = await fetchT(`https://${domain}/`, {
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
agent: httpsAgent,
|
agent: httpsAgent
|
||||||
timeout: 5000
|
}, 5000);
|
||||||
});
|
|
||||||
|
|
||||||
log.info('caddy', 'Site is accessible', { domain, status: response.status });
|
log.info('caddy', 'Site is accessible', { domain, status: response.status });
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ async function refreshDnsToken(username, password, server, fetchT, log) {
|
|||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
},
|
|
||||||
timeout: 10000
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
10000
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ function createProviderDnsContext(siteConfig, buildDomain, credentialManager, fe
|
|||||||
const params = new URLSearchParams({ user: username, pass: password, includeInfo: 'false' });
|
const params = new URLSearchParams({ user: username, pass: password, includeInfo: 'false' });
|
||||||
const response = await fetchT(
|
const response = await fetchT(
|
||||||
`http://${server}:5380/api/user/login?${params.toString()}`,
|
`http://${server}:5380/api/user/login?${params.toString()}`,
|
||||||
{ method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 10000 }
|
{ method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' } },
|
||||||
|
10000
|
||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.status === 'ok' && result.token) {
|
if (result.status === 'ok' && result.token) {
|
||||||
|
|||||||
@@ -38,7 +38,15 @@ function fetchT(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
|||||||
if (!opts.signal) {
|
if (!opts.signal) {
|
||||||
opts = { ...opts, signal: AbortSignal.timeout(timeoutMs) };
|
opts = { ...opts, signal: AbortSignal.timeout(timeoutMs) };
|
||||||
}
|
}
|
||||||
delete opts.timeout;
|
// The `timeout` key in fetch() opts is silently ignored by undici. Callers
|
||||||
|
// should use the third arg of fetchT() (timeoutMs) instead. If a caller
|
||||||
|
// passes `timeout: N` here, it's almost certainly a bug — we used to silently
|
||||||
|
// strip it, which masked the issue. Now we surface it in logs and strip it.
|
||||||
|
if ('timeout' in opts) {
|
||||||
|
console.warn(`[fetchT] opts.timeout=${opts.timeout} is ignored — pass timeoutMs as the 3rd arg of fetchT() instead. Called from: ${new Error().stack.split('\n').slice(2, 4).join(' <- ')}`);
|
||||||
|
const { timeout, ...rest } = opts;
|
||||||
|
opts = rest;
|
||||||
|
}
|
||||||
return fetch(url, opts);
|
return fetch(url, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.status === 'success') {
|
if (result.success) {
|
||||||
const select = document.getElementById('existing-ca-select');
|
const select = document.getElementById('existing-ca-select');
|
||||||
select.innerHTML = '';
|
select.innerHTML = '';
|
||||||
|
|
||||||
if (result.data.cas.length === 0) {
|
if (result.cas.length === 0) {
|
||||||
select.innerHTML = '<option value="">No CAs found in Caddyfile</option>';
|
select.innerHTML = '<option value="">No CAs found in Caddyfile</option>';
|
||||||
} else {
|
} else {
|
||||||
select.innerHTML = '<option value="">Select existing CA...</option>';
|
select.innerHTML = '<option value="">Select existing CA...</option>';
|
||||||
result.data.cas.forEach(ca => {
|
result.cas.forEach(ca => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
if (typeof ca === 'object') {
|
if (typeof ca === 'object') {
|
||||||
option.value = ca.id;
|
option.value = ca.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user