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 * @param {Function} deps.ok - Success response helper * @returns {express.Router} */ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError, ok }) { 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 // /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 secretOk = presentedBuf.length === expectedBuf.length && presentedBuf.length > 0 && require('crypto').timingSafeEqual(presentedBuf, expectedBuf); if (!secretOk) { return res.status(401).json({ success: false, error: '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; };