From 20c74b81b6dd99a806845b9af78f1d349c9a99fa Mon Sep 17 00:00:00 2001 From: Rishnu Dk Date: Thu, 12 Feb 2026 14:59:32 +0530 Subject: [PATCH] Add Firebase cloud backup and restore flow --- README.md | 23 ++++++ manifest.json | 12 ++- newtab.html | 23 ++++++ script.js | 214 +++++++++++++++++++++++++++++++++++++++++++++++++- style.css | 44 +++++++++++ 5 files changed, 309 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ad0a4ca..e1ce794 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,26 @@ Gmail, Drive, YouTube, Meet, Calendar, Sheets, Gemini ## ⭐ If you like BookmarkFolder Give the repo a **star** 🌟 on GitHub β€” it helps a lot! + + +## ☁️ Firebase Cloud Backup (optional) + +You can now back up and restore your bookmarks using Firebase so reinstalling the extension won’t lose your data. + +1. Create a Firebase project +2. Enable **Authentication β†’ Email/Password** +3. Enable **Firestore Database** (start in test mode while developing) +4. Open the new tab page and enter: + - Firebase Web API Key + - Firebase Project ID +5. Sign up / sign in from the sidebar panel +6. Use **Backup to Cloud** and **Restore from Cloud** + +### Firestore document path used +`bookmarkfolderUsers/{firebaseLocalUserId}` + +Each document stores: +- `data` (stringified JSON payload) +- `updatedAt` (unix timestamp in ms) + +> Security note: set Firestore security rules so users can only read/write their own doc. diff --git a/manifest.json b/manifest.json index 11f2107..95e8dc0 100644 --- a/manifest.json +++ b/manifest.json @@ -2,10 +2,14 @@ "manifest_version": 3, "name": "Rishnu Dk-custom bookmark tab", "version": "1.0", - - "permissions": ["storage"], - + "permissions": [ + "storage" + ], "chrome_url_overrides": { "newtab": "newtab.html" - } + }, + "host_permissions": [ + "https://identitytoolkit.googleapis.com/*", + "https://firestore.googleapis.com/*" + ] } diff --git a/newtab.html b/newtab.html index 951027f..7b1bd6e 100644 --- a/newtab.html +++ b/newtab.html @@ -24,6 +24,29 @@

Bookmarks

diff --git a/script.js b/script.js index 1ddeb61..a41d308 100644 --- a/script.js +++ b/script.js @@ -10,12 +10,16 @@ let openFolders = {}; let dragSourceTile = null; let dragPlaceholder = null; +let firebaseConfigState = { apiKey: "", projectId: "" }; +let firebaseAuthState = { idToken: "", localId: "", email: "" }; +let lastUpdatedAt = Date.now(); + // --------------------------------------------------------- // LOAD EVERYTHING // --------------------------------------------------------- function loadAll() { - chrome.storage.sync.get(["bookmarks", "folders", "openFolders"], res => { + chrome.storage.sync.get(["bookmarks", "folders", "openFolders", "firebaseConfig", "firebaseAuth", "lastUpdatedAt"], async res => { const raw = res.bookmarks || []; @@ -35,11 +39,18 @@ function loadAll() { } }); - chrome.storage.sync.set({ bookmarks, folders, openFolders }); + firebaseConfigState = res.firebaseConfig || { apiKey: "", projectId: "" }; + lastUpdatedAt = res.lastUpdatedAt || Date.now(); + firebaseAuthState = res.firebaseAuth || { idToken: "", localId: "", email: "" }; + + chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt }); renderFolders(); populateFolderSelect(); renderGoogleApps(); + initializeCloudUI(); + + await restoreFromCloud(false); }); } loadAll(); @@ -48,7 +59,9 @@ loadAll(); // SAVE ALL // --------------------------------------------------------- function saveAll() { - chrome.storage.sync.set({ bookmarks, folders, openFolders }); + lastUpdatedAt = Date.now(); + chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt }); + backupToCloud(false); } // --------------------------------------------------------- @@ -584,3 +597,198 @@ function renderGoogleApps() { div.appendChild(item); }); } + + +// --------------------------------------------------------- +// FIREBASE CLOUD BACKUP (REST API) +// --------------------------------------------------------- +function initializeCloudUI() { + const apiKeyInput = document.getElementById("firebaseApiKey"); + const projectIdInput = document.getElementById("firebaseProjectId"); + const emailInput = document.getElementById("authEmail"); + const passwordInput = document.getElementById("authPassword"); + + if (!apiKeyInput || !projectIdInput) return; + + apiKeyInput.value = firebaseConfigState.apiKey || ""; + projectIdInput.value = firebaseConfigState.projectId || ""; + emailInput.value = firebaseAuthState.email || ""; + + apiKeyInput.addEventListener("change", () => { + firebaseConfigState.apiKey = apiKeyInput.value.trim(); + persistCloudState(); + setCloudStatus("Firebase config updated."); + }); + + projectIdInput.addEventListener("change", () => { + firebaseConfigState.projectId = projectIdInput.value.trim(); + persistCloudState(); + setCloudStatus("Firebase config updated."); + }); + + document.getElementById("signUpBtn")?.addEventListener("click", () => authWithFirebase("signUp")); + document.getElementById("signInBtn")?.addEventListener("click", () => authWithFirebase("signInWithPassword")); + document.getElementById("signOutBtn")?.addEventListener("click", signOutFirebase); + document.getElementById("backupNowBtn")?.addEventListener("click", () => backupToCloud(true)); + document.getElementById("restoreNowBtn")?.addEventListener("click", () => restoreFromCloud(true)); + + updateCloudStatusFromState(); +} + +function persistCloudState() { + chrome.storage.sync.set({ firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState }); +} + +function setCloudStatus(message, isError = false) { + const status = document.getElementById("cloudStatus"); + if (!status) return; + status.textContent = message; + status.style.color = isError ? "#fca5a5" : "#93c5fd"; +} + +function updateCloudStatusFromState() { + if (firebaseAuthState.email) { + setCloudStatus(`Connected as ${firebaseAuthState.email}`); + } else { + setCloudStatus("Not connected"); + } +} + +function cloudConfigReady() { + return Boolean(firebaseConfigState.apiKey && firebaseConfigState.projectId); +} + +function cloudAuthReady() { + return Boolean(firebaseAuthState.idToken && firebaseAuthState.localId); +} + +function cloudPayload() { + return { + bookmarks, + folders, + openFolders, + updatedAt: lastUpdatedAt + }; +} + +async function authWithFirebase(mode) { + const email = document.getElementById("authEmail")?.value.trim(); + const password = document.getElementById("authPassword")?.value.trim(); + + if (!cloudConfigReady()) return setCloudStatus("Add Firebase API Key + Project ID first.", true); + if (!email || !password) return setCloudStatus("Enter email and password.", true); + + try { + const endpoint = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${firebaseConfigState.apiKey}`; + const res = await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, returnSecureToken: true }) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error?.message || "Authentication failed"); + + firebaseAuthState = { idToken: data.idToken, localId: data.localId, email: data.email || email }; + persistCloudState(); + updateCloudStatusFromState(); + await restoreFromCloud(false); + } catch (err) { + setCloudStatus(`Auth failed: ${err.message}`, true); + } +} + +function signOutFirebase() { + firebaseAuthState = { idToken: "", localId: "", email: "" }; + persistCloudState(); + updateCloudStatusFromState(); +} + +function cloudDocumentUrl() { + return `https://firestore.googleapis.com/v1/projects/${firebaseConfigState.projectId}/databases/(default)/documents/bookmarkfolderUsers/${firebaseAuthState.localId}`; +} + +async function backupToCloud(showMessage = true) { + if (!cloudConfigReady() || !cloudAuthReady()) { + if (showMessage) setCloudStatus("Cloud backup skipped: sign in first.", true); + return; + } + + try { + const payload = cloudPayload(); + const body = { + fields: { + data: { stringValue: JSON.stringify(payload) }, + updatedAt: { integerValue: String(payload.updatedAt) } + } + }; + + const res = await fetch(cloudDocumentUrl(), { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${firebaseAuthState.idToken}` + }, + body: JSON.stringify(body) + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.error?.message || "Backup failed"); + + if (showMessage) setCloudStatus("Backup complete."); + } catch (err) { + setCloudStatus(`Backup failed: ${err.message}`, true); + } +} + +async function restoreFromCloud(showMessage = true) { + if (!cloudConfigReady() || !cloudAuthReady()) { + if (showMessage) setCloudStatus("Cloud restore skipped: sign in first.", true); + return; + } + + try { + const res = await fetch(cloudDocumentUrl(), { + headers: { Authorization: `Bearer ${firebaseAuthState.idToken}` } + }); + + if (res.status === 404) { + if (showMessage) setCloudStatus("No cloud backup found yet."); + return; + } + + const data = await res.json(); + if (!res.ok) throw new Error(data.error?.message || "Restore failed"); + + const encoded = data.fields?.data?.stringValue; + if (!encoded) { + if (showMessage) setCloudStatus("Cloud document is empty.", true); + return; + } + + const cloud = JSON.parse(encoded); + const localUpdatedAt = Number(lastUpdatedAt || 0); + const cloudUpdatedAt = Number(cloud.updatedAt || 0); + + if (cloudUpdatedAt < localUpdatedAt) { + if (showMessage) setCloudStatus("Local data is newer than cloud."); + return; + } + + bookmarks = Array.isArray(cloud.bookmarks) ? cloud.bookmarks : []; + folders = Array.isArray(cloud.folders) && cloud.folders.length ? cloud.folders : ["Default"]; + openFolders = cloud.openFolders && typeof cloud.openFolders === "object" ? cloud.openFolders : {}; + + folders.forEach(f => { + if (openFolders[f] === undefined) openFolders[f] = f === "Default"; + }); + + saveAll(); + renderFolders(); + populateFolderSelect(); + + if (showMessage) setCloudStatus("Restore complete."); + } catch (err) { + setCloudStatus(`Restore failed: ${err.message}`, true); + } +} diff --git a/style.css b/style.css index f6224e3..cc4ebd2 100644 --- a/style.css +++ b/style.css @@ -723,3 +723,47 @@ body::before { height: 60px; } } + +/* =================================== + FIREBASE CLOUD PANEL + =================================== */ +.cloud-panel { + margin-top: 16px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 12px; +} + +.cloud-panel h3 { + margin-bottom: 10px; + font-size: 14px; + color: #bfdbfe; +} + +.cloud-panel input { + width: 100%; + padding: 10px; + margin-bottom: 8px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(15, 23, 42, 0.6); + color: #e2e8f0; +} + +.cloud-actions { + display: grid; + gap: 8px; + grid-template-columns: 1fr; +} + +.cloud-actions .small-btn { + margin-bottom: 0; +} + +.cloud-status { + margin-top: 10px; + font-size: 12px; + color: #93c5fd; + line-height: 1.4; +}