From 122c3a96c7cb1cbf8293c9d78de9b70ab7875ccd Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 9 Feb 2026 12:27:36 +0500 Subject: [PATCH] web: fix account recovery --- apps/web/src/common/db.ts | 31 ++++-- apps/web/src/common/sqlite/sqlite.worker.ts | 2 + apps/web/src/interfaces/key-store.ts | 12 ++- apps/web/src/root.tsx | 19 ++-- apps/web/src/utils/feature-check.ts | 6 +- apps/web/src/views/recovery.tsx | 113 +------------------- packages/core/src/api/user-manager.ts | 97 +++++++++-------- 7 files changed, 99 insertions(+), 181 deletions(-) diff --git a/apps/web/src/common/db.ts b/apps/web/src/common/db.ts index fb0e7cbdc..37941f339 100644 --- a/apps/web/src/common/db.ts +++ b/apps/web/src/common/db.ts @@ -48,15 +48,23 @@ async function initializeDatabase(persistence: DatabasePersistence) { await useKeyStore.getState().setValue("databaseKey", databaseKey); } + // db.host({ + // API_HOST: "https://api.notesnook.com", + // AUTH_HOST: "https://auth.streetwriters.co", + // SSE_HOST: "https://events.streetwriters.co", + // ISSUES_HOST: "https://issues.streetwriters.co", + // SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co", + // MONOGRAPH_HOST: "https://monogr.ph", + // NOTESNOOK_HOST: "https://notesnook.com", + // ...Config.get("serverUrls", {}) + // }); + const base = `http://localhost`; db.host({ - API_HOST: "https://api.notesnook.com", - AUTH_HOST: "https://auth.streetwriters.co", - SSE_HOST: "https://events.streetwriters.co", - ISSUES_HOST: "https://issues.streetwriters.co", - SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co", - MONOGRAPH_HOST: "https://monogr.ph", - NOTESNOOK_HOST: "https://notesnook.com", - ...Config.get("serverUrls", {}) + API_HOST: `${base}:5264`, + AUTH_HOST: `${base}:8264`, + SSE_HOST: `${base}:7264`, + ISSUES_HOST: `${base}:2624`, + SUBSCRIPTIONS_HOST: `${base}:9264` }); const storage = new NNStorage( @@ -72,7 +80,7 @@ async function initializeDatabase(persistence: DatabasePersistence) { dialect: (name, init) => createDialect({ name: persistence === "memory" ? ":memory:" : name, - encrypted: true, + encrypted: persistence !== "memory", async: !isFeatureSupported("opfs"), init, multiTab @@ -87,7 +95,10 @@ async function initializeDatabase(persistence: DatabasePersistence) { synchronous: "normal", pageSize: 8192, cacheSize: -32000, - password: Buffer.from(databaseKey).toString("hex"), + password: + persistence === "memory" + ? undefined + : Buffer.from(databaseKey).toString("hex"), skipInitialization: !IS_DESKTOP_APP && multiTab }, storage: storage, diff --git a/apps/web/src/common/sqlite/sqlite.worker.ts b/apps/web/src/common/sqlite/sqlite.worker.ts index 36929cf03..f72e420d2 100644 --- a/apps/web/src/common/sqlite/sqlite.worker.ts +++ b/apps/web/src/common/sqlite/sqlite.worker.ts @@ -167,6 +167,8 @@ class _SQLiteWorker { sql: string, parameters?: SQLiteCompatibleType[] ): Promise> { + if (!this.encrypted && !this.initialized) await this.initialize(); + if (this.encrypted && !sql.startsWith("PRAGMA key")) { await this.waitForDatabase(); } diff --git a/apps/web/src/interfaces/key-store.ts b/apps/web/src/interfaces/key-store.ts index 75b270e43..3f54f0d9a 100644 --- a/apps/web/src/interfaces/key-store.ts +++ b/apps/web/src/interfaces/key-store.ts @@ -146,13 +146,19 @@ class KeyStore extends BaseStore { activeCredentials = () => this.get().credentials.filter((c) => c.active); - init = async () => { + init = async ( + config: { persistence: "memory" | "db" } = { persistence: "db" } + ) => { this.#metadataStore = - isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey") + isFeatureSupported("indexedDB") && + isFeatureSupported("clonableCryptoKey") && + config.persistence !== "memory" ? new IndexedDBKVStore(`${this.dbName}-metadata`, "metadata") : new MemoryKVStore(); this.#secretStore = - isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey") + isFeatureSupported("indexedDB") && + isFeatureSupported("clonableCryptoKey") && + config.persistence !== "memory" ? new IndexedDBKVStore(`${this.dbName}-secrets`, "secrets") : new MemoryKVStore(); diff --git a/apps/web/src/root.tsx b/apps/web/src/root.tsx index 5de2050da..1f621ba72 100644 --- a/apps/web/src/root.tsx +++ b/apps/web/src/root.tsx @@ -54,7 +54,12 @@ export async function startApp(children?: React.ReactNode) { try { const { Component, props, path } = await init(); - await useKeyStore.getState().init(); + const persistence = + (path !== "/sessionexpired" && path !== "/account/recovery") || + Config.get("sessionExpired", false) + ? "db" + : "memory"; + await useKeyStore.getState().init({ persistence }); root.render( <> @@ -70,6 +75,7 @@ export async function startApp(children?: React.ReactNode) { Component={Component} path={path} routeProps={props} + persistence={persistence} /> {children} @@ -96,9 +102,10 @@ function RouteWrapper(props: { Component: (props: AuthProps) => JSX.Element; path: Routes; routeProps: AuthProps | null; + persistence?: "db" | "memory"; }) { const [isMigrating, setIsMigrating] = useState(false); - const { Component, path, routeProps } = props; + const { Component, path, routeProps, persistence } = props; useEffect(() => { EV.subscribe(EVENTS.migrationStarted, (name) => @@ -112,12 +119,8 @@ function RouteWrapper(props: { const result = usePromise(async () => { performance.mark("load:database"); - await loadDatabase( - path !== "/sessionexpired" || Config.get("sessionExpired", false) - ? "db" - : "memory" - ); - }, [path]); + await loadDatabase(persistence); + }, [path, persistence]); if (result.status === "rejected") { throw result.reason instanceof Error diff --git a/apps/web/src/utils/feature-check.ts b/apps/web/src/utils/feature-check.ts index 116413874..dac2c487e 100644 --- a/apps/web/src/utils/feature-check.ts +++ b/apps/web/src/utils/feature-check.ts @@ -34,11 +34,11 @@ export function isTransferableStreamsSupported() { controller.close(); } }); - window.postMessage(readable, [readable]); + window.postMessage(readable, window.location.origin, [readable]); FEATURE_CHECKS.transferableStreams = true; return true; - } catch { - console.log("Transferable streams not supported"); + } catch (e) { + console.log("Transferable streams not supported", e); FEATURE_CHECKS.transferableStreams = false; return false; } diff --git a/apps/web/src/views/recovery.tsx b/apps/web/src/views/recovery.tsx index a045df736..c602cc806 100644 --- a/apps/web/src/views/recovery.tsx +++ b/apps/web/src/views/recovery.tsx @@ -25,7 +25,6 @@ import { Loader } from "../components/loader"; import { showToast } from "../utils/toast"; import AuthContainer from "../components/auth-container"; import { AuthField, SubmitButton } from "./auth"; -import { createBackup, restoreBackupFile, selectBackupFile } from "../common"; import Config from "../utils/config"; import { ErrorText } from "../components/error-text"; import { EVENTS, User } from "@notesnook/core"; @@ -33,18 +32,14 @@ import { RecoveryKeyDialog } from "../dialogs/recovery-key-dialog"; import { strings } from "@notesnook/intl"; import { useKeyStore } from "../interfaces/key-store"; -type RecoveryMethodType = "key" | "backup" | "reset"; +type RecoveryMethodType = "key" | "reset"; type RecoveryMethodsFormData = Record; type RecoveryKeyFormData = { recoveryKey: string; }; -type BackupFileFormData = { - backupFile: File; -}; - -type NewPasswordFormData = BackupFileFormData & { +type NewPasswordFormData = { userResetRequired?: boolean; password: string; confirmPassword: string; @@ -53,9 +48,7 @@ type NewPasswordFormData = BackupFileFormData & { type RecoveryFormData = { methods: RecoveryMethodsFormData; "method:key": RecoveryKeyFormData; - "method:backup": BackupFileFormData; "method:reset": NewPasswordFormData; - backup: RecoveryMethodsFormData; new: NewPasswordFormData; final: RecoveryMethodsFormData; }; @@ -73,9 +66,7 @@ type BaseRecoveryComponentProps = { type RecoveryRoutes = | "methods" | "method:key" - | "method:backup" | "method:reset" - | "backup" | "new" | "final"; type RecoveryProps = { route: RecoveryRoutes }; @@ -92,10 +83,6 @@ function getRouteComponent( return RecoveryMethods as RecoveryComponent; case "method:key": return RecoveryKeyMethod as RecoveryComponent; - case "method:backup": - return BackupFileMethod as RecoveryComponent; - case "backup": - return BackupData as RecoveryComponent; case "method:reset": case "new": return NewPassword as RecoveryComponent; @@ -108,9 +95,7 @@ function getRouteComponent( const routePaths: Record = { methods: "/account/recovery/methods", "method:key": "/account/recovery/method/key", - "method:backup": "/account/recovery/method/backup", "method:reset": "/account/recovery/method/reset", - backup: "/account/recovery/backup", new: "/account/recovery/new", final: "/account/recovery/final" }; @@ -240,12 +225,6 @@ const recoveryMethods: RecoveryMethod[] = [ title: () => strings.recoveryKeyMethod(), description: () => strings.recoveryKeyMethodDesc() }, - { - type: "backup", - testId: "step-backup", - title: () => strings.backupFileMethod(), - description: () => strings.backupFileMethodDesc() - }, { type: "reset", testId: "step-reset-account", @@ -356,8 +335,7 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) { await useKeyStore .getState() .setValue("userEncryptionKey", form.recoveryKey); - await db.sync({ type: "fetch", force: true }); - navigate("backup"); + navigate("new", {}); }} > ) { ); } -function BackupFileMethod(props: BaseRecoveryComponentProps<"method:backup">) { - const { navigate } = props; - const [backupFile, setBackupFile] = - useState(); - - useEffect(() => { - if (!backupFile) return; - const backupFileInput = document.getElementById("backupFile"); - if (!(backupFileInput instanceof HTMLInputElement)) return; - backupFileInput.value = backupFile?.name; - }, [backupFile]); - - return ( - - } - onSubmit={async () => { - navigate("new", { backupFile, userResetRequired: true }); - }} - > - {strings.browse()}, - onClick: async () => { - setBackupFile(await selectBackupFile()); - } - }} - /> - - - - - ); -} - -function BackupData(props: BaseRecoveryComponentProps<"backup">) { - const { navigate } = props; - - return ( - { - await createBackup({ rescueMode: true, mode: "full" }); - navigate("new"); - }} - > - - - ); -} - function NewPassword(props: BaseRecoveryComponentProps<"new">) { const { navigate, formData } = props; const [progress, setProgress] = useState(0); @@ -498,11 +396,6 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) { if (!(await db.user.resetPassword(form.password))) throw new Error("Could not reset account password."); - if (formData?.backupFile) { - await restoreBackupFile(formData?.backupFile); - await db.sync({ type: "full", force: true }); - } - navigate("final"); }} > diff --git a/packages/core/src/api/user-manager.ts b/packages/core/src/api/user-manager.ts index 3376a07f4..f9aa82d56 100644 --- a/packages/core/src/api/user-manager.ts +++ b/packages/core/src/api/user-manager.ts @@ -628,58 +628,61 @@ class UserManager { if (!new_password) throw new Error("New password is required."); data.encryptionKey = data.encryptionKey || (await this.getMasterKey()); - if (!data.encryptionKey) - throw new Error("User encryption key not generated."); - - const newMasterKey = await this.db - .storage() - .generateCryptoKey(new_password, salt); const updateUserPayload: Partial = {}; - if (user.attachmentsKey) { - updateUserPayload.attachmentsKey = await this.keyManager.rewrapKey( - user.attachmentsKey, - data.encryptionKey, - newMasterKey - ); - } - if (user.monographPasswordsKey) { - updateUserPayload.monographPasswordsKey = await this.keyManager.rewrapKey( - user.monographPasswordsKey, - data.encryptionKey, - newMasterKey - ); - } - if (user.inboxKeys) { - updateUserPayload.inboxKeys = await this.keyManager.rewrapKey( - user.inboxKeys, - data.encryptionKey, - newMasterKey - ); - } - - if (user.legacyDataEncryptionKey) - updateUserPayload.legacyDataEncryptionKey = - await this.keyManager.rewrapKey( - user.legacyDataEncryptionKey, + console.log( + "Has encryption key", + !!data.encryptionKey, + await this.getMasterKey() + ); + if (data.encryptionKey) { + const newMasterKey = await this.db + .storage() + .generateCryptoKey(new_password, salt); + if (user.attachmentsKey) { + updateUserPayload.attachmentsKey = await this.keyManager.rewrapKey( + user.attachmentsKey, data.encryptionKey, newMasterKey ); - if (user.dataEncryptionKey) - updateUserPayload.dataEncryptionKey = await this.keyManager.rewrapKey( - user.dataEncryptionKey, - data.encryptionKey, - newMasterKey - ); - else { - updateUserPayload.dataEncryptionKey = await this.keyManager.wrapKey( - await this.db.crypto().generateRandomKey(), - newMasterKey - ); - updateUserPayload.legacyDataEncryptionKey = await this.keyManager.wrapKey( - data.encryptionKey, - newMasterKey - ); + } + if (user.monographPasswordsKey) { + updateUserPayload.monographPasswordsKey = + await this.keyManager.rewrapKey( + user.monographPasswordsKey, + data.encryptionKey, + newMasterKey + ); + } + if (user.inboxKeys) { + updateUserPayload.inboxKeys = await this.keyManager.rewrapKey( + user.inboxKeys, + data.encryptionKey, + newMasterKey + ); + } + + if (user.legacyDataEncryptionKey) + updateUserPayload.legacyDataEncryptionKey = + await this.keyManager.rewrapKey( + user.legacyDataEncryptionKey, + data.encryptionKey, + newMasterKey + ); + if (user.dataEncryptionKey) + updateUserPayload.dataEncryptionKey = await this.keyManager.rewrapKey( + user.dataEncryptionKey, + data.encryptionKey, + newMasterKey + ); + else { + updateUserPayload.dataEncryptionKey = await this.keyManager.wrapKey( + await this.db.crypto().generateRandomKey(), + newMasterKey + ); + updateUserPayload.legacyDataEncryptionKey = + await this.keyManager.wrapKey(data.encryptionKey, newMasterKey); + } } await http.patch.json(