After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:
1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths
Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.
Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)
Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
174 lines
7.1 KiB
JavaScript
174 lines
7.1 KiB
JavaScript
const express = require('express');
|
|
const { paginate, parsePaginationParams } = require('../src/utilities/pagination');
|
|
const { ValidationError } = require('../src/utilities/errors');
|
|
const { ok, successMessage } = require('../src/utils/responses');
|
|
|
|
/**
|
|
* Updates route factory
|
|
* @param {Object} deps - Explicit dependencies
|
|
* @param {Object} deps.updateManager - Container update manager
|
|
* @param {Object} deps.selfUpdater - DashCaddy self-update manager
|
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
|
* @param {Function} deps.logError - Error logging function
|
|
* @returns {express.Router}
|
|
*/
|
|
module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }) {
|
|
const router = express.Router();
|
|
|
|
// ===== UPDATE MANAGEMENT ENDPOINTS =====
|
|
|
|
// Check for updates
|
|
router.post('/updates/check', asyncHandler(async (req, res) => {
|
|
await updateManager.checkForUpdates();
|
|
const updates = updateManager.getAvailableUpdates();
|
|
ok(res, { updates, count: updates.length });
|
|
}, 'updates-check'));
|
|
|
|
// Get available updates
|
|
router.get('/updates/available', asyncHandler(async (req, res) => {
|
|
const updates = updateManager.getAvailableUpdates();
|
|
const paginationParams = parsePaginationParams(req.query);
|
|
const result = paginate(updates, paginationParams);
|
|
ok(res, { updates: result.data, count: updates.length, ...(result.pagination && { pagination: result.pagination }) });
|
|
}, 'updates-available'));
|
|
|
|
// Update a container
|
|
router.post('/updates/update/:containerId', asyncHandler(async (req, res) => {
|
|
const result = await updateManager.updateContainer(req.params.containerId, req.body);
|
|
ok(res, { result });
|
|
}, 'updates-update'));
|
|
|
|
// Rollback update
|
|
router.post('/updates/rollback/:containerId', asyncHandler(async (req, res) => {
|
|
await updateManager.rollbackUpdate(req.params.containerId);
|
|
successMessage(res, 'Rollback completed');
|
|
}, 'updates-rollback'));
|
|
|
|
// Get update history
|
|
router.get('/updates/history', asyncHandler(async (req, res) => {
|
|
const paginationParams = parsePaginationParams(req.query);
|
|
// When paginating, fetch all history so pagination can slice correctly
|
|
const fetchLimit = paginationParams ? Number.MAX_SAFE_INTEGER : (parseInt(req.query.limit) || 50);
|
|
const history = updateManager.getHistory(fetchLimit);
|
|
const result = paginate(history, paginationParams);
|
|
ok(res, { history: result.data, ...(result.pagination && { pagination: result.pagination }) });
|
|
}, 'updates-history'));
|
|
|
|
// Configure auto-update
|
|
router.post('/updates/auto-update/:containerId', asyncHandler(async (req, res) => {
|
|
updateManager.configureAutoUpdate(req.params.containerId, req.body);
|
|
successMessage(res, 'Auto-update configured');
|
|
}, 'updates-auto-update'));
|
|
|
|
// Get auto-update configuration
|
|
router.get('/updates/auto-update', asyncHandler(async (req, res) => {
|
|
const config = updateManager.getAutoUpdateConfig();
|
|
ok(res, { config });
|
|
}, 'updates-auto-update-config'));
|
|
|
|
// Schedule update
|
|
router.post('/updates/schedule/:containerId', asyncHandler(async (req, res) => {
|
|
const { scheduledTime } = req.body;
|
|
if (!scheduledTime) {
|
|
throw new ValidationError('scheduledTime is required');
|
|
}
|
|
updateManager.scheduleUpdate(req.params.containerId, scheduledTime);
|
|
ok(res, { message: 'Update scheduled', scheduledTime });
|
|
}, 'updates-schedule'));
|
|
|
|
// ===== DASHCADDY SELF-UPDATE ENDPOINTS =====
|
|
|
|
// Get current version
|
|
router.get('/system/version', asyncHandler(async (req, res) => {
|
|
const local = selfUpdater.getLocalVersion();
|
|
ok(res, { name: 'DashCaddy', version: local.version, commit: local.commit });
|
|
}, 'system-version'));
|
|
|
|
// Check for DashCaddy update
|
|
router.get('/system/update-check', asyncHandler(async (req, res) => {
|
|
const result = await selfUpdater.checkForUpdate();
|
|
ok(res, result);
|
|
}, 'system-update-check'));
|
|
|
|
// Apply available update
|
|
router.post('/system/update-apply', asyncHandler(async (req, res) => {
|
|
const check = await selfUpdater.checkForUpdate();
|
|
if (!check.available) {
|
|
return successMessage(res, 'Already up to date');
|
|
}
|
|
// Refuse same-version applies. The check.available flag can theoretically be
|
|
// true with equal versions (commit-mismatch path); applying anyway just
|
|
// rebuilds the container without changing anything user-visible and pollutes
|
|
// history with v1.4.0 → v1.4.0 entries.
|
|
const localV = check.local && check.local.version;
|
|
const remoteV = check.remote && check.remote.version;
|
|
if (localV && remoteV && localV === remoteV) {
|
|
return ok(res, { message: 'Already up to date', version: localV });
|
|
}
|
|
// Start async — container may restart
|
|
selfUpdater.applyUpdate(check.remote).catch(err => {
|
|
logError('self-update', err);
|
|
});
|
|
ok(res, {
|
|
message: 'Update initiated',
|
|
fromVersion: localV,
|
|
toVersion: remoteV,
|
|
});
|
|
}, 'system-update-apply'));
|
|
|
|
// Notify endpoint — the publishing host POSTs here when a new release is
|
|
// out so the instance can update within seconds instead of waiting for the
|
|
// next 30-min poll. Auth is a shared secret in X-DashCaddy-Notify-Secret
|
|
// (per-instance, generated on first start, lives at
|
|
// <updatesDir>/notify-secret). This route is in the public-routes allowlist
|
|
// because TOTP would block machine-to-machine notifies.
|
|
router.post('/system/update-notify', asyncHandler(async (req, res) => {
|
|
const presented = req.get('X-DashCaddy-Notify-Secret') || '';
|
|
const expected = selfUpdater.getNotifySecret() || '';
|
|
// constant-time compare to avoid timing leaks
|
|
const presentedBuf = Buffer.from(presented);
|
|
const expectedBuf = Buffer.from(expected);
|
|
const ok = presentedBuf.length === expectedBuf.length &&
|
|
presentedBuf.length > 0 &&
|
|
require('crypto').timingSafeEqual(presentedBuf, expectedBuf);
|
|
if (!ok) {
|
|
return unauthorized(res, 'Invalid notify secret');
|
|
}
|
|
const result = selfUpdater.notifyAndApply('http-notify');
|
|
ok(res, result);
|
|
}, 'system-update-notify'));
|
|
|
|
// Get update status
|
|
router.get('/system/update-status', asyncHandler(async (req, res) => {
|
|
ok(res, {
|
|
status: selfUpdater.getStatus(),
|
|
lastCheck: selfUpdater.lastCheckTime,
|
|
lastResult: selfUpdater.lastCheckResult,
|
|
});
|
|
}, 'system-update-status'));
|
|
|
|
// Get self-update history
|
|
router.get('/system/update-history', asyncHandler(async (req, res) => {
|
|
const history = selfUpdater.getUpdateHistory();
|
|
ok(res, { history });
|
|
}, 'system-update-history'));
|
|
|
|
// List rollback versions
|
|
router.get('/system/rollback-versions', asyncHandler(async (req, res) => {
|
|
const versions = selfUpdater.getAvailableRollbacks();
|
|
ok(res, { versions });
|
|
}, 'system-rollback-versions'));
|
|
|
|
// Rollback to a previous version
|
|
router.post('/system/rollback', asyncHandler(async (req, res) => {
|
|
const { version } = req.body;
|
|
if (!version) throw new ValidationError('version is required');
|
|
selfUpdater.rollbackToVersion(version).catch(err => {
|
|
logError('self-rollback', err);
|
|
});
|
|
ok(res, { message: `Rollback to ${version} initiated` });
|
|
}, 'system-rollback'));
|
|
|
|
return router;
|
|
};
|