Merge krystie-improvements into main
Resolves 24 conflicts between Hermes (DC-008/009/010 + response-helper
envelope standardization) and Krystie (DC-005 src/ refactor path fixes,
DC-006 TOTP integration, DC-007 new test suites, cloud backup
destinations).
Conflict resolutions:
- src/utils/logging.js: took ours (consumers depend on logError/
safeErrorMessage/createLogger exports)
- src/config/site.js: merged (her factored validateAndLogConfig +
applyConfigFields helpers)
- src/context/dns.js: took hers (admin/readonly role iteration for
write operations)
- src/utilities/backup-
manager.js: took hers (Dropbox/WebDAV/SFTP cloud feature)
- status/dist/*, status/
sw.js: took hers (minified bundles + newer SW cache)
Additional fix (post-merge regression):
- src/monitoring/health-checker.js: fixed DC-005 path miss —
'require(./platform-paths)' → 'require(../../platform-paths)'
Test status: 921/922 passing. One known failure in logging.test.js
(async file-handle timing) tracked as follow-up.
This commit is contained in:
@@ -4,13 +4,14 @@ const http = require('http');
|
||||
const https = require('https');
|
||||
const tls = require('tls');
|
||||
const validatorLib = require('validator');
|
||||
const { APP, REGEX, TIMEOUTS } = require('../constants');
|
||||
const { validateServiceConfig, isValidPort } = require('../input-validator');
|
||||
const { exists } = require('../fs-helpers');
|
||||
const { paginate, parsePaginationParams } = require('../pagination');
|
||||
const { ValidationError, NotFoundError, ConflictError } = require('../errors');
|
||||
const { resolveServiceUrl } = require('../url-resolver');
|
||||
const { success, error: errorResponse } = require('../response-helpers');
|
||||
const { APP, REGEX, TIMEOUTS } = require('../src/utilities/constants');
|
||||
const { validateServiceConfig, isValidPort } = require('../src/security/input-validator');
|
||||
const { exists } = require('../src/utilities/fs-helpers');
|
||||
const { paginate, parsePaginationParams } = require('../src/utilities/pagination');
|
||||
const { ValidationError, NotFoundError, ConflictError } = require('../src/utilities/errors');
|
||||
const { resolveServiceUrl } = require('../src/utilities/url-resolver');
|
||||
const { success, error: errorResponse } = require('../src/utils/responses');
|
||||
const platformPaths = require('../platform-paths');
|
||||
|
||||
/**
|
||||
* Services route factory
|
||||
@@ -46,7 +47,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;
|
||||
|
||||
@@ -355,9 +356,11 @@ module.exports = function({
|
||||
}, 'services-status'));
|
||||
|
||||
// List all services
|
||||
// Always returns the standard envelope. The `services` field is the array
|
||||
// (paginated if ?page=N&limit=M is in the query, otherwise the full list).
|
||||
router.get('/services', asyncHandler(async (req, res) => {
|
||||
if (!await exists(SERVICES_FILE)) {
|
||||
return res.json([]);
|
||||
return success(res, { services: [] });
|
||||
}
|
||||
const services = await servicesStateManager.read();
|
||||
const paginationParams = parsePaginationParams(req.query);
|
||||
@@ -365,14 +368,14 @@ module.exports = function({
|
||||
if (paginationParams) {
|
||||
success(res, { services: result.data, pagination: result.pagination });
|
||||
} else {
|
||||
res.json(result.data);
|
||||
success(res, { services: result.data });
|
||||
}
|
||||
}, 'services-list'));
|
||||
|
||||
// Add a new service
|
||||
router.post('/services', asyncHandler(async (req, res) => {
|
||||
try {
|
||||
const { id, name, logo } = req.body;
|
||||
const { id, name, logo, category, containerId, port, ip, tailscaleOnly } = req.body;
|
||||
|
||||
if (!id || !name) {
|
||||
throw new ValidationError('id and name are required');
|
||||
@@ -391,7 +394,14 @@ module.exports = function({
|
||||
throw new ConflictError(`Service "${id}" already exists`, id);
|
||||
}
|
||||
|
||||
services.push({ id, name, logo: logo || `/assets/${id}.png` });
|
||||
const newService = { id, name, logo: logo || `/assets/${id}.png` };
|
||||
// Persist optional metadata fields if provided
|
||||
if (category) newService.category = category;
|
||||
if (containerId) newService.containerId = containerId;
|
||||
if (port) newService.port = port;
|
||||
if (ip) newService.ip = ip;
|
||||
if (typeof tailscaleOnly === 'boolean') newService.tailscaleOnly = tailscaleOnly;
|
||||
services.push(newService);
|
||||
return services;
|
||||
});
|
||||
|
||||
@@ -513,9 +523,8 @@ module.exports = function({
|
||||
|
||||
if (oldSubdomain !== newSubdomain) {
|
||||
try {
|
||||
const dnsToken = dns.getToken();
|
||||
await dns.call(siteConfig.dnsServerIp, '/api/zones/records/delete', { token: dnsToken, domain: oldDomain, type: 'A' });
|
||||
await dns.createRecord(newSubdomain, ip || 'localhost');
|
||||
await dns.universalDeleteRecord(oldDomain);
|
||||
await dns.universalCreateRecord(newSubdomain, ip || 'localhost');
|
||||
results.dns = 'updated';
|
||||
} catch (e) {
|
||||
results.dns = `failed: ${e.message}`;
|
||||
@@ -542,6 +551,8 @@ module.exports = function({
|
||||
};
|
||||
if (name) services[serviceIndex].name = name;
|
||||
if (logo) services[serviceIndex].logo = logo;
|
||||
// Allow category update via update endpoint too (optional body field)
|
||||
if (req.body.category !== undefined) services[serviceIndex].category = req.body.category || undefined;
|
||||
results.services = 'updated';
|
||||
} else {
|
||||
results.services = 'not found';
|
||||
|
||||
Reference in New Issue
Block a user