From 1e2f6b989aa3dc45226d68080c7ca2c805b13fd2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 25 Jan 2026 21:33:00 -0500 Subject: [PATCH] Migrate legacy R2 settings to storage service --- docs/storage-r2.md | 4 +- js/r2.js | 43 +++----- js/services/r2Service.js | 190 +++++----------------------------- js/services/storageService.js | 81 +++++++++++++++ 4 files changed, 123 insertions(+), 195 deletions(-) diff --git a/docs/storage-r2.md b/docs/storage-r2.md index a030d85a..f7157265 100644 --- a/docs/storage-r2.md +++ b/docs/storage-r2.md @@ -6,7 +6,7 @@ > **Security Note:** This integration requires storing encrypted credentials in the browser. See [`README.md`](../README.md) and [`docs/storage-tab-design.md`](./storage-tab-design.md) for security warnings and alternative upload modes for untrusted environments. -While the new **Storage Tab** (powered by `StorageService`) manages the secure storage of credentials, `r2Service` is responsible for: +The **Storage Tab** (powered by `StorageService`) is the single supported configuration path for R2 credentials. `r2Service` is responsible for: - Uploading the selected video/HLS asset via multipart S3 uploads. - Verifying public access via the configured Public Bucket URL. @@ -69,7 +69,7 @@ The app will attempt to upload a small test file to verify the credentials and p ## Configuration keys -R2 credentials are no longer stored in plain `bitvidSettings`. They are encapsulated within the `StorageService` data model. +R2 credentials are no longer stored in plain `bitvidSettings`. They are encapsulated within the `StorageService` data model. Legacy `r2Settings` are migrated into `StorageService` on first unlock and then cleared. **R2 Connection Profile (Encrypted):** - `provider`: `cloudflare_r2` diff --git a/js/r2.js b/js/r2.js index fed1bdd7..0c8d2776 100644 --- a/js/r2.js +++ b/js/r2.js @@ -92,7 +92,12 @@ function openSettingsDb() { }); } -export async function loadR2Settings() { +/** + * Legacy R2 settings loader (deprecated). + * Used only for one-time migration into StorageService. + * @deprecated + */ +export async function loadLegacyR2Settings() { try { const db = await openSettingsDb(); if (db) { @@ -126,36 +131,12 @@ export async function loadR2Settings() { return normalizeSettings(null); } -export async function saveR2Settings(settings) { - const normalized = normalizeSettings(settings); - - let db = null; - try { - db = await openSettingsDb(); - } catch (err) { - userLogger.warn("Unable to open IndexedDB, continuing with fallback:", err); - } - - if (db) { - await new Promise((resolve, reject) => { - const tx = db.transaction(STORE_NAME, "readwrite"); - const store = tx.objectStore(STORE_NAME); - const req = store.put(normalized, SETTINGS_KEY); - req.onsuccess = () => resolve(); - req.onerror = () => - reject(req.error || new Error("Failed to save R2 settings")); - }); - } else if (typeof localStorage !== "undefined") { - localStorage.setItem( - LOCALSTORAGE_FALLBACK_KEY, - JSON.stringify(normalized) - ); - } - - return normalized; -} - -export async function clearR2Settings() { +/** + * Legacy R2 settings clearer (deprecated). + * Used only after successful migration into StorageService. + * @deprecated + */ +export async function clearLegacyR2Settings() { let cleared = false; try { const db = await openSettingsDb(); diff --git a/js/services/r2Service.js b/js/services/r2Service.js index 943fb060..34e962d2 100644 --- a/js/services/r2Service.js +++ b/js/services/r2Service.js @@ -17,13 +17,8 @@ */ import { - loadR2Settings, - saveR2Settings, - clearR2Settings, buildR2Key, buildPublicUrl, - mergeBucketEntry, - sanitizeBaseDomain, } from "../r2.js"; import { sanitizeBucketName, @@ -290,101 +285,24 @@ class R2Service { } async loadSettings() { - try { - const settings = await loadR2Settings(); - this.setSettings(settings); - this.populateCloudflareSettingsInputs(settings); - return settings; - } catch (err) { - userLogger.error("Failed to load Cloudflare settings:", err); - this.setSettings(createDefaultSettings()); - this.populateCloudflareSettingsInputs(this.getSettings()); - this.setCloudflareSettingsStatus( - "Failed to load saved settings.", - "error" - ); - throw err; - } + const settings = createDefaultSettings(); + this.setSettings(settings); + this.populateCloudflareSettingsInputs(settings); + this.setCloudflareSettingsStatus( + "Legacy Cloudflare settings have been retired. Use the Storage tab.", + "warning" + ); + return settings; } async handleCloudflareSettingsSubmit(formValues = {}, { quiet = false } = {}) { - // Legacy support wrapper: redirects to storageService if possible, or warns. - // For now, we allow saving to legacy for backward compatibility if explicit flow isn't used, - // but we prefer to use storageService. - - const accountId = String(formValues.accountId || "").trim(); - const accessKeyId = String(formValues.accessKeyId || "").trim(); - const secretAccessKey = String(formValues.secretAccessKey || "").trim(); - const baseDomain = sanitizeBaseDomain(formValues.baseDomain || ""); // This is the Public Bucket URL - - if (baseDomain.includes(".r2.cloudflarestorage.com")) { - if (!quiet) { - this.setCloudflareSettingsStatus( - "It looks like you entered the S3 API URL. Please use your Public Bucket URL (e.g. https://pub-xxx.r2.dev or your custom domain).", - "error" - ); - } - return false; - } - - if (!accountId || !accessKeyId || !secretAccessKey) { - if (!quiet) { - this.setCloudflareSettingsStatus( - "Account ID, Access Key ID, and Secret are required.", - "error" - ); - } - return false; - } - - if (!baseDomain) { - if (!quiet) { - this.setCloudflareSettingsStatus( - "Public Bucket URL is required (e.g., https://pub-xxx.r2.dev).", - "error" - ); - } - return false; - } - - // Try to save to StorageService if available and unlocked - // We can't easily know the active pubkey here unless passed or inferred from context. - // Since this method is legacy, we'll default to legacy behavior but warn. - - let buckets = { ...(this.getSettings().buckets || {}) }; - const previousAccount = this.getSettings().accountId || ""; - const previousBaseDomain = this.getSettings().baseDomain || ""; - - if (previousAccount !== accountId || previousBaseDomain !== baseDomain) { - buckets = {}; - } - - const updatedSettings = { - accountId, - accessKeyId, - secretAccessKey, - baseDomain, - buckets, - }; - - try { - const saved = await saveR2Settings(updatedSettings); - this.setSettings(saved); - this.populateCloudflareSettingsInputs(saved); - if (!quiet) { - this.setCloudflareSettingsStatus("Settings saved locally.", "success"); - } - return true; - } catch (err) { - userLogger.error("Failed to save Cloudflare settings:", err); - if (!quiet) { - this.setCloudflareSettingsStatus( - "Failed to save settings. Check console for details.", - "error" - ); - } - return false; + if (!quiet) { + this.setCloudflareSettingsStatus( + "Legacy settings are disabled. Configure Cloudflare R2 in the Storage tab.", + "error" + ); } + return false; } async saveSettings(formValues = {}, options = {}) { @@ -462,33 +380,17 @@ class R2Service { } } - // 2. Fallback to legacy - const legacy = this.cloudflareSettings; - if ( - legacy && - legacy.accountId && - legacy.accessKeyId && - legacy.secretAccessKey - ) { - return { ...legacy, isLegacy: true }; - } - return null; } async handleCloudflareClearSettings() { - try { - await clearR2Settings(); - const refreshed = await loadR2Settings(); - this.setSettings(refreshed); - this.populateCloudflareSettingsInputs(refreshed); - this.setCloudflareSettingsStatus("Settings cleared.", "success"); - return true; - } catch (err) { - userLogger.error("Failed to clear Cloudflare settings:", err); - this.setCloudflareSettingsStatus("Failed to clear settings.", "error"); - return false; - } + this.setSettings(createDefaultSettings()); + this.populateCloudflareSettingsInputs(this.getSettings()); + this.setCloudflareSettingsStatus( + "Legacy settings are no longer supported. Clear settings in the Storage tab.", + "warning" + ); + return false; } async clearSettings() { @@ -607,35 +509,6 @@ class R2Service { lastUpdated: Date.now(), }; - // If using legacy settings, we save the bucket mapping. - // If using StorageService (settings.isLegacy === false), we do NOT save to legacy store - // to avoid partial state or overwriting. We just return the manual entry. - if (settings.isLegacy !== false) { - const currentSettings = this.getSettings(); - if (!this.cloudflareSettings) { - this.cloudflareSettings = currentSettings; - } - let entry = currentSettings.buckets?.[npub]; - let savedEntry = entry; - - if ( - !entry || - entry.bucket !== manualEntry.bucket || - entry.publicBaseUrl !== manualEntry.publicBaseUrl - ) { - const updatedSettings = await saveR2Settings( - mergeBucketEntry(currentSettings, npub, manualEntry) - ); - this.setSettings(updatedSettings); - savedEntry = updatedSettings.buckets?.[npub] || manualEntry; - } - return { - entry: savedEntry, - usedManagedFallback: false, - customDomainStatus: "manual", - }; - } - return { entry: manualEntry, usedManagedFallback: false, @@ -797,7 +670,7 @@ class R2Service { bucket: effectiveSettings.bucket || "", baseDomain: effectiveSettings.baseDomain || "", publicBaseUrl: effectiveSettings.publicBaseUrl || "", - isLegacy: effectiveSettings.isLegacy !== false, + legacyFallback: Boolean(effectiveSettings.isLegacy), }); if (!accountId || !accessKeyId || !secretAccessKey) { @@ -858,19 +731,12 @@ class R2Service { forcedTorrentKey = "", forcedTorrentUrl = "", } = {}) { - // If no explicit credentials, we might save settingsInput to legacy DB. - // If explicitCredentials ARE provided, we skip saving and use them directly. - if (!explicitCredentials && settingsInput) { - const saved = await this.handleCloudflareSettingsSubmit(settingsInput, { - quiet: true, - }); - if (!saved) { - this.setCloudflareUploadStatus( - "Fix your R2 settings before uploading.", - "error" - ); - return false; - } + if (settingsInput) { + this.setCloudflareUploadStatus( + "Legacy Cloudflare settings are disabled. Configure R2 in the Storage tab.", + "error" + ); + return false; } if (!npub) { diff --git a/js/services/storageService.js b/js/services/storageService.js index a4bcd9de..177ac829 100644 --- a/js/services/storageService.js +++ b/js/services/storageService.js @@ -1,4 +1,5 @@ import { userLogger } from "../utils/logger.js"; +import { clearLegacyR2Settings, loadLegacyR2Settings } from "../r2.js"; import { bytesToHex } from "../../vendor/crypto-helpers.bundle.min.js"; import { testS3Connection } from "../storage/r2-s3.js"; @@ -109,6 +110,18 @@ function buildS3TestConfig(config, overrides = {}) { }; } +function encodeNpub(pubkey) { + if (!pubkey) return null; + try { + if (typeof window !== "undefined" && window.NostrTools?.nip19?.npubEncode) { + return window.NostrTools.nip19.npubEncode(pubkey); + } + } catch (err) { + userLogger.warn("[StorageService] Failed to encode npub for migration:", err); + } + return null; +} + const PROVIDER_TESTS = { [PROVIDERS.R2]: (config) => testS3Connection( @@ -404,6 +417,8 @@ export class StorageService { this.masterKeys.set(pubkey, masterKey); userLogger.log(`[StorageService] Unlocked storage for ${pubkey.slice(0, 8)}...`); + + await this._migrateLegacyR2Settings(pubkey); } /** @@ -587,6 +602,72 @@ export class StorageService { return this.testAccess(connection.provider, connection); } + + async _migrateLegacyR2Settings(pubkey) { + let legacySettings = null; + try { + legacySettings = await loadLegacyR2Settings(); + } catch (err) { + userLogger.warn("[StorageService] Legacy R2 settings load failed:", err); + return; + } + + if ( + !legacySettings || + !legacySettings.accountId || + !legacySettings.accessKeyId || + !legacySettings.secretAccessKey || + !legacySettings.baseDomain + ) { + return; + } + + const existingConnections = await this.listConnections(pubkey); + const hasR2 = existingConnections.some( + (connection) => connection.provider === PROVIDERS.R2 + ); + if (hasR2) { + return; + } + + const npub = encodeNpub(pubkey); + const legacyEntry = + npub && legacySettings.buckets ? legacySettings.buckets[npub] : null; + const publicBaseUrl = + legacyEntry?.publicBaseUrl || legacySettings.baseDomain || ""; + + const payload = { + provider: PROVIDERS.R2, + accountId: legacySettings.accountId, + accessKeyId: legacySettings.accessKeyId, + secretAccessKey: legacySettings.secretAccessKey, + bucket: legacyEntry?.bucket || "", + }; + const meta = { + provider: PROVIDERS.R2, + label: "Cloudflare R2 (Migrated)", + publicBaseUrl, + baseDomain: publicBaseUrl, + defaultForUploads: existingConnections.length === 0, + migratedFromLegacy: true, + }; + + const baseId = "cloudflare-r2-migrated"; + let connectionId = baseId; + let suffix = 1; + while (existingConnections.some((conn) => conn.id === connectionId)) { + connectionId = `${baseId}-${suffix}`; + suffix += 1; + } + + try { + await this.saveConnection(pubkey, connectionId, payload, meta); + await clearLegacyR2Settings(); + userLogger.log("[StorageService] Migrated legacy R2 settings."); + } catch (err) { + userLogger.warn("[StorageService] Legacy R2 migration failed:", err); + } + } } const storageService = new StorageService();