- Top-bar calendar widget (status/js/calendar-widget.js): next-appointment
display, settings modal with CalDAV/ICS source management and a
Test-connection probe. Follows the weather-widget pattern.
- Backend routes/calendar.js: sources CRUD (secrets in credential manager,
never in responses), aggregated events across sources with per-source
error isolation, Radicale provision endpoint, CalDAV discover probe.
- src/calendar/ics-parser.js: dependency-free RFC 5545 parser with bounded
RRULE expansion (DAILY/WEEKLY-BYDAY/MONTHLY/YEARLY, COUNT/UNTIL/INTERVAL,
EXDATE consumes COUNT, RECURRENCE-ID overrides), 500-event cap.
- src/calendar/caldav-client.js: CalDAV REPORT time-range queries over
node:https (no new deps), HTTPS-enforced off localhost, 5MiB response cap.
- src/calendar/radicale-provisioner.js: zero-config auto-wiring - generates
a 24-char password, writes Radicale htpasswd config (pure-Node APR1 hash
verified byte-for-byte against openssl passwd -apr1 vectors), stores
credentials, registers the source. Deploy hook in routes/apps/deploy.js
wires any Radicale deployed via the App Selector automatically.
- Radicale app template (tomsquest/docker-radicale) in the catalog.
- Tests: 32 new (parser unit, provisioner unit incl. openssl vectors,
route tests). Full suite 3552 passed; only pre-existing pdf-parse
billing/invoice failures remain (proven red at base 448f535).
- OpenAPI: 5 calendar paths documented. CHANGELOG entry added.
264 lines
7.3 KiB
JavaScript
264 lines
7.3 KiB
JavaScript
/**
|
|
* Context assembly - Dependency injection container
|
|
* Assembles all context objects needed by routes
|
|
*/
|
|
const { createDockerContext } = require('./docker');
|
|
const { createCaddyContext } = require('./caddy');
|
|
const { createDnsContext } = require('./dns');
|
|
const { createSessionContext } = require('./session');
|
|
const NotificationManager = require('../managers/notification-manager');
|
|
const tailscaleManager = require('../managers/tailscale-manager');
|
|
const { TailscaleCoordClient } = require('../managers/tailscale-coord');
|
|
const fs = require('fs');
|
|
|
|
/**
|
|
* Assemble the full application context
|
|
* This replaces the old "god object" ctx with explicit construction
|
|
*/
|
|
function assembleContext({
|
|
// Config
|
|
siteConfig,
|
|
buildDomain,
|
|
buildServiceUrl,
|
|
SERVICES_FILE,
|
|
CONFIG_FILE,
|
|
TOTP_CONFIG_FILE,
|
|
TAILSCALE_CONFIG_FILE,
|
|
NOTIFICATIONS_FILE,
|
|
ERROR_LOG_FILE,
|
|
DNS_CREDENTIALS_FILE,
|
|
CADDYFILE_PATH,
|
|
CADDY_ADMIN_URL,
|
|
|
|
// State managers
|
|
servicesStateManager,
|
|
configStateManager,
|
|
calendarStateManager,
|
|
|
|
// DC-053 share store
|
|
shareStore,
|
|
|
|
// Managers
|
|
credentialManager,
|
|
authManager,
|
|
licenseManager,
|
|
healthChecker,
|
|
updateManager,
|
|
backupManager,
|
|
resourceMonitor,
|
|
auditLogger,
|
|
portLockManager,
|
|
selfUpdater,
|
|
dockerMaintenance,
|
|
logDigest,
|
|
dockerSecurity,
|
|
|
|
// Templates
|
|
APP_TEMPLATES,
|
|
TEMPLATE_CATEGORIES,
|
|
DIFFICULTY_LEVELS,
|
|
RECIPE_TEMPLATES,
|
|
RECIPE_CATEGORIES,
|
|
|
|
// Helpers
|
|
asyncHandler,
|
|
errorResponse,
|
|
ok,
|
|
fetchT,
|
|
httpsAgent,
|
|
log,
|
|
logError,
|
|
safeErrorMessage,
|
|
getServiceById,
|
|
readConfig,
|
|
saveConfig,
|
|
addServiceToConfig,
|
|
validateURL,
|
|
strictLimiter,
|
|
totpConfig,
|
|
saveTotpConfig,
|
|
loadSiteConfig,
|
|
loadNotificationConfig,
|
|
resyncHealthChecker,
|
|
|
|
// Middleware result
|
|
middlewareResult,
|
|
|
|
// App
|
|
app,
|
|
}) {
|
|
// Create domain-specific contexts
|
|
const docker = createDockerContext(dockerSecurity);
|
|
const caddy = createCaddyContext(CADDYFILE_PATH, CADDY_ADMIN_URL, fetchT, httpsAgent, log, siteConfig, buildDomain);
|
|
const dns = createDnsContext(siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log, DNS_CREDENTIALS_FILE);
|
|
const session = createSessionContext(middlewareResult);
|
|
|
|
// Create notification manager
|
|
const notification = new NotificationManager({
|
|
NOTIFICATIONS_FILE,
|
|
fetchT,
|
|
docker,
|
|
log,
|
|
config: siteConfig
|
|
});
|
|
|
|
// --- Tailscale coordination API client --------------------------------------
|
|
// Reads the API token from credentialManager on every call (not cached on
|
|
// the client) so that PUT /api/v1/tailscale/settings takes effect
|
|
// immediately without restarting the process. The metadata file
|
|
// tailscale-config.json stores non-secret state (tailnet name, last
|
|
// validation time, device count) so we don't have to hit the API just to
|
|
// answer "is this configured?" in the UI.
|
|
function loadTailscaleMetadata() {
|
|
try {
|
|
if (TAILSCALE_CONFIG_FILE && fs.existsSync(TAILSCALE_CONFIG_FILE)) {
|
|
return JSON.parse(fs.readFileSync(TAILSCALE_CONFIG_FILE, 'utf8'));
|
|
}
|
|
} catch (_e) { /* corrupt file → treat as unconfigured */ }
|
|
return { configured: false };
|
|
}
|
|
function saveTailscaleMetadata(meta) {
|
|
if (!TAILSCALE_CONFIG_FILE) return;
|
|
try {
|
|
fs.writeFileSync(TAILSCALE_CONFIG_FILE, JSON.stringify(meta, null, 2), 'utf8');
|
|
} catch (e) {
|
|
log.error('tailscale-coord', e, null, { note: 'Failed to write tailscale-config.json' });
|
|
}
|
|
}
|
|
async function getCoordClient() {
|
|
const tok = await credentialManager.retrieve('tailscale.coord.apiToken');
|
|
return new TailscaleCoordClient({ apiToken: tok || null });
|
|
}
|
|
|
|
// Assemble flat context (temporary - routes still expect this)
|
|
// Note: tailscale interface detection lives in src/utilities/network-detector.js
|
|
// (DC-031). The empty `tailscale` stub previously wired here was dead code
|
|
// — verified zero readers via grep across src/.
|
|
const ctx = {
|
|
// Namespaced contexts
|
|
docker,
|
|
caddy,
|
|
dns,
|
|
session,
|
|
notification,
|
|
// Tailscale manager — wraps `tailscale status --json` with 5min cache.
|
|
// Replaces the long-standing null stub at src/app.js:189. See
|
|
// src/managers/tailscale-manager.js for full API surface.
|
|
tailscale: {
|
|
getStatus: tailscaleManager.getStatus,
|
|
getLocalIP: tailscaleManager.getLocalIP,
|
|
getSummary: tailscaleManager.getSummary,
|
|
getDevices: tailscaleManager.getDevices,
|
|
isTailscaleIP: tailscaleManager.isTailscaleIP,
|
|
invalidateCache: tailscaleManager.invalidateCache,
|
|
getAccessToken: tailscaleManager.getAccessToken,
|
|
startSyncTimer: tailscaleManager.startSyncTimer,
|
|
stopSyncTimer: tailscaleManager.stopSyncTimer,
|
|
syncAPI: tailscaleManager.syncAPI,
|
|
},
|
|
|
|
// Tailscale coordination API client — talk to api.tailscale.com for
|
|
// device management, pre-auth key creation, ACL reads/writes, and user
|
|
// listing. Distinct from the local tailscaleManager above (which reads
|
|
// the local tailscaled daemon). The API token is stored encrypted via
|
|
// credentialManager and re-read on every call so settings changes take
|
|
// effect without process restart.
|
|
tailscaleCoord: {
|
|
// Returns a fresh client each call — cheap (just a Map + token lookup),
|
|
// and guarantees the latest token is used.
|
|
getClient: getCoordClient,
|
|
// Metadata helpers — read/write tailscale-config.json
|
|
loadMetadata: loadTailscaleMetadata,
|
|
saveMetadata: saveTailscaleMetadata,
|
|
// Storage helpers — wraps credentialManager so route code doesn't
|
|
// need to know the key naming convention.
|
|
setApiToken: async (token) => {
|
|
if (token) {
|
|
await credentialManager.store('tailscale.coord.apiToken', token, {
|
|
description: 'Tailscale coordination API token',
|
|
source: 'settings-ui',
|
|
});
|
|
} else {
|
|
await credentialManager.delete('tailscale.coord.apiToken');
|
|
}
|
|
},
|
|
hasApiToken: async () => {
|
|
const tok = await credentialManager.retrieve('tailscale.coord.apiToken');
|
|
return !!tok;
|
|
},
|
|
},
|
|
|
|
// App and config
|
|
app,
|
|
siteConfig,
|
|
|
|
// State managers
|
|
servicesStateManager,
|
|
configStateManager,
|
|
calendarStateManager,
|
|
|
|
// DC-053 share store
|
|
shareStore,
|
|
|
|
// Managers
|
|
credentialManager,
|
|
authManager,
|
|
licenseManager,
|
|
healthChecker,
|
|
updateManager,
|
|
backupManager,
|
|
resourceMonitor,
|
|
auditLogger,
|
|
portLockManager,
|
|
selfUpdater,
|
|
dockerMaintenance,
|
|
logDigest,
|
|
|
|
// Templates
|
|
APP_TEMPLATES,
|
|
TEMPLATE_CATEGORIES,
|
|
DIFFICULTY_LEVELS,
|
|
RECIPE_TEMPLATES,
|
|
RECIPE_CATEGORIES,
|
|
|
|
// Helpers
|
|
asyncHandler,
|
|
errorResponse,
|
|
ok,
|
|
fetchT,
|
|
log,
|
|
logError,
|
|
safeErrorMessage,
|
|
buildDomain,
|
|
buildServiceUrl,
|
|
getServiceById,
|
|
readConfig,
|
|
saveConfig,
|
|
addServiceToConfig,
|
|
validateURL,
|
|
strictLimiter,
|
|
|
|
// Config helpers
|
|
totpConfig,
|
|
saveTotpConfig,
|
|
loadSiteConfig,
|
|
loadNotificationConfig,
|
|
resyncHealthChecker,
|
|
|
|
// Middleware result (exposes renewCSRFToken etc.)
|
|
middlewareResult,
|
|
|
|
// File paths
|
|
SERVICES_FILE,
|
|
CONFIG_FILE,
|
|
TOTP_CONFIG_FILE,
|
|
TAILSCALE_CONFIG_FILE,
|
|
NOTIFICATIONS_FILE,
|
|
ERROR_LOG_FILE,
|
|
};
|
|
|
|
return ctx;
|
|
}
|
|
|
|
module.exports = { assembleContext };
|