diff --git a/dashcaddy-api/crypto-utils.js b/dashcaddy-api/crypto-utils.js index f534f10..49fb7f9 100644 --- a/dashcaddy-api/crypto-utils.js +++ b/dashcaddy-api/crypto-utils.js @@ -312,8 +312,9 @@ function decryptWithKey(encryptedData, key) { return decrypted; } -// Initialize key on module load -loadOrCreateKey(); +// Lazy-initialize: key is loaded on first encrypt/decrypt call. +// Do NOT call loadOrCreateKey() here — during Docker build, it would generate +// a key baked into the image that conflicts with the mounted production key. /** * Clear the cached encryption key so it reloads from file on next use. diff --git a/dashcaddy-api/license-manager.js b/dashcaddy-api/license-manager.js index cda3568..341b743 100644 --- a/dashcaddy-api/license-manager.js +++ b/dashcaddy-api/license-manager.js @@ -37,14 +37,16 @@ class LicenseManager { } /** - * Load license state from storage on startup + * Load license state from storage on startup. + * Primary: encrypted credential store. Fallback: config.json backup. + * If the credential store fails (e.g. encryption key changed after rebuild), + * restores from the config.json backup automatically. */ async load() { try { const stored = await this.credentialManager.retrieve(LICENSE_CRED_KEY); if (stored) { this.activation = JSON.parse(stored); - // Check if expired if (this.isExpired()) { this.log.info?.('license', 'License has expired', { code: this._maskCode(this.activation.code), @@ -57,13 +59,40 @@ class LicenseManager { daysRemaining: this.daysRemaining() }); } - } else { - this.log.info?.('license', 'No active license'); + this._loaded = true; + return; } } catch (error) { - this.log.error?.('license', 'Failed to load license state', { error: error.message }); - this.activation = null; + this.log.warn?.('license', 'Failed to load from credential store, trying config backup', { error: error.message }); } + + // Fallback: restore from config.json backup + try { + const fsp = require('fs').promises; + const data = await fsp.readFile(this.configFile, 'utf8'); + const config = JSON.parse(data); + if (config.licenseBackup) { + this.activation = config.licenseBackup; + this.log.info?.('license', 'License restored from config backup', { + code: this._maskCode(this.activation.code), + lifetime: this.activation.lifetime + }); + // Re-store in credential manager so future loads succeed + try { + await this.credentialManager.store(LICENSE_CRED_KEY, JSON.stringify(this.activation)); + this.log.info?.('license', 'License re-stored in credential manager'); + } catch (storeErr) { + this.log.warn?.('license', 'Could not re-store license in credential manager', { error: storeErr.message }); + } + this._loaded = true; + return; + } + } catch (_) { + // Config doesn't exist or no backup — continue + } + + this.log.info?.('license', 'No active license'); + this.activation = null; this._loaded = true; } @@ -412,7 +441,9 @@ class LicenseManager { } /** - * Update config.json with non-sensitive license info + * Update config.json with license info and full activation backup. + * The backup ensures the license survives encryption key changes + * (e.g. container rebuilds that generate new keys). */ async _updateConfig() { try { @@ -433,8 +464,11 @@ class LicenseManager { daysRemaining: this.daysRemaining(), features: this.activation.features || Object.keys(PREMIUM_FEATURES) }; + // Full backup of activation data (config.json is volume-mounted and persists) + config.licenseBackup = this.activation; } else { config.license = { active: false, tier: 'free' }; + delete config.licenseBackup; } config.updatedAt = new Date().toISOString();