mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
web: fix account recovery
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -167,6 +167,8 @@ class _SQLiteWorker {
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (!this.encrypted && !this.initialized) await this.initialize();
|
||||
|
||||
if (this.encrypted && !sql.startsWith("PRAGMA key")) {
|
||||
await this.waitForDatabase();
|
||||
}
|
||||
|
||||
@@ -146,13 +146,19 @@ class KeyStore extends BaseStore<KeyStore> {
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</AppLock>
|
||||
{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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
|
||||
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<TRoute extends RecoveryRoutes> = {
|
||||
type RecoveryRoutes =
|
||||
| "methods"
|
||||
| "method:key"
|
||||
| "method:backup"
|
||||
| "method:reset"
|
||||
| "backup"
|
||||
| "new"
|
||||
| "final";
|
||||
type RecoveryProps = { route: RecoveryRoutes };
|
||||
@@ -92,10 +83,6 @@ function getRouteComponent<TRoute extends RecoveryRoutes>(
|
||||
return RecoveryMethods as RecoveryComponent<TRoute>;
|
||||
case "method:key":
|
||||
return RecoveryKeyMethod as RecoveryComponent<TRoute>;
|
||||
case "method:backup":
|
||||
return BackupFileMethod as RecoveryComponent<TRoute>;
|
||||
case "backup":
|
||||
return BackupData as RecoveryComponent<TRoute>;
|
||||
case "method:reset":
|
||||
case "new":
|
||||
return NewPassword as RecoveryComponent<TRoute>;
|
||||
@@ -108,9 +95,7 @@ function getRouteComponent<TRoute extends RecoveryRoutes>(
|
||||
const routePaths: Record<RecoveryRoutes, string> = {
|
||||
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", {});
|
||||
}}
|
||||
>
|
||||
<AuthField
|
||||
@@ -383,86 +361,6 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
|
||||
);
|
||||
}
|
||||
|
||||
function BackupFileMethod(props: BaseRecoveryComponentProps<"method:backup">) {
|
||||
const { navigate } = props;
|
||||
const [backupFile, setBackupFile] =
|
||||
useState<BackupFileFormData["backupFile"]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!backupFile) return;
|
||||
const backupFileInput = document.getElementById("backupFile");
|
||||
if (!(backupFileInput instanceof HTMLInputElement)) return;
|
||||
backupFileInput.value = backupFile?.name;
|
||||
}, [backupFile]);
|
||||
|
||||
return (
|
||||
<RecoveryForm
|
||||
testId="step-backup-file"
|
||||
type="method:backup"
|
||||
title={strings.accountRecovery()}
|
||||
subtitle={
|
||||
<ErrorText
|
||||
sx={{ fontSize: "body" }}
|
||||
error={strings.backupFileRecoveryError()}
|
||||
/>
|
||||
}
|
||||
onSubmit={async () => {
|
||||
navigate("new", { backupFile, userResetRequired: true });
|
||||
}}
|
||||
>
|
||||
<AuthField
|
||||
id="backupFile"
|
||||
type="text"
|
||||
label={strings.selectBackupFile()}
|
||||
helpText={strings.backupFileHelpText()}
|
||||
autoComplete="none"
|
||||
autoFocus
|
||||
disabled
|
||||
action={{
|
||||
component: <Text variant={"body"}>{strings.browse()}</Text>,
|
||||
onClick: async () => {
|
||||
setBackupFile(await selectBackupFile());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SubmitButton text={strings.startAccountRecovery()} />
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
mt={4}
|
||||
variant={"anchor"}
|
||||
onClick={() => navigate("methods")}
|
||||
sx={{ color: "paragraph" }}
|
||||
>
|
||||
{strings.dontHaveBackupFile()}
|
||||
</Button>
|
||||
</RecoveryForm>
|
||||
);
|
||||
}
|
||||
|
||||
function BackupData(props: BaseRecoveryComponentProps<"backup">) {
|
||||
const { navigate } = props;
|
||||
|
||||
return (
|
||||
<RecoveryForm
|
||||
testId="step-backup-data"
|
||||
type="backup"
|
||||
title={strings.backupYourData()}
|
||||
subtitle={strings.backupYourDataDesc()}
|
||||
loading={{
|
||||
title: strings.backingUpData() + "...",
|
||||
subtitle: strings.backingUpDataWait()
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
await createBackup({ rescueMode: true, mode: "full" });
|
||||
navigate("new");
|
||||
}}
|
||||
>
|
||||
<SubmitButton text={strings.downloadBackupFile()} />
|
||||
</RecoveryForm>
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<User> = {};
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user