Move to service layer

This commit is contained in:
Manav Rathi
2025-06-11 09:32:33 +05:30
parent fc21932a34
commit 644bfe72af
4 changed files with 121 additions and 80 deletions

View File

@@ -11,7 +11,7 @@ import { LinkButton } from "ente-base/components/LinkButton";
import { LoadingButton } from "ente-base/components/mui/LoadingButton";
import { isHTTPErrorWithStatus } from "ente-base/http";
import log from "ente-base/log";
import { getData, setLSUser } from "ente-shared/storage/localStorage";
import { getData } from "ente-shared/storage/localStorage";
import { Formik, type FormikHelpers } from "formik";
import { t } from "i18next";
import { useRouter } from "next/router";
@@ -86,7 +86,6 @@ const ChangeEmailForm: React.FC = () => {
try {
setLoading(true);
await changeEmail(email, ott!);
await setLSUser({ ...getData("user"), email });
setLoading(false);
void goToApp();
} catch (e) {

View File

@@ -8,30 +8,18 @@ import SetPasswordForm, {
} from "ente-accounts/components/SetPasswordForm";
import { appHomeRoute, stashRedirect } from "ente-accounts/services/redirect";
import {
generateSRPSetupAttributes,
getSRPAttributes,
updateSRPAndKeyAttributes,
type UpdatedKeyAttr,
} from "ente-accounts/services/srp";
import {
ensureSavedKeyAttributes,
generateAndSaveInteractiveKeyAttributes,
changePassword,
localUser,
type LocalUser,
} from "ente-accounts/services/user";
import { LinkButton } from "ente-base/components/LinkButton";
import { LoadingIndicator } from "ente-base/components/loaders";
import { deriveSensitiveKey, encryptBox } from "ente-base/crypto";
import { deriveKeyInsufficientMemoryErrorMessage } from "ente-base/crypto/types";
import log from "ente-base/log";
import {
ensureMasterKeyFromSession,
saveMasterKeyInSessionAndSafeStore,
} from "ente-base/session";
import { getData, setData } from "ente-shared/storage/localStorage";
import { t } from "i18next";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
/**
* A page that allows a user to reset or change their password.
@@ -63,71 +51,27 @@ interface PageContentsProps {
const PageContents: React.FC<PageContentsProps> = ({ user }) => {
const router = useRouter();
const redirectToAppHome = useCallback(() => {
setData("showBackButton", { value: true });
void router.push(appHomeRoute);
}, [router]);
const onSubmit: SetPasswordFormProps["callback"] = async (
password,
setFieldError,
) => {
try {
await onSubmit2(password);
} catch (e) {
log.error("Could not change password", e);
setFieldError(
"confirm",
e instanceof Error &&
e.message == deriveKeyInsufficientMemoryErrorMessage
? t("password_generation_failed")
: t("generic_error"),
);
}
};
const onSubmit2 = async (password: string) => {
const masterKey = await ensureMasterKeyFromSession();
const keyAttributes = ensureSavedKeyAttributes();
const {
key: kek,
salt: kekSalt,
opsLimit,
memLimit,
} = await deriveSensitiveKey(password);
const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } =
await encryptBox(masterKey, kek);
const updatedKeyAttr: UpdatedKeyAttr = {
encryptedKey,
keyDecryptionNonce,
kekSalt,
opsLimit,
memLimit,
};
await updateSRPAndKeyAttributes(
await generateSRPSetupAttributes(kek),
updatedKeyAttr,
);
// Update the SRP attributes that are stored locally.
const srpAttributes = await getSRPAttributes(user.email);
if (srpAttributes) {
setData("srpAttributes", srpAttributes);
}
await generateAndSaveInteractiveKeyAttributes(
password,
{ ...keyAttributes, ...updatedKeyAttr },
masterKey,
);
await saveMasterKeyInSessionAndSafeStore(masterKey);
redirectToAppHome();
};
const redirectToAppHome = () => {
setData("showBackButton", { value: true });
void router.push(appHomeRoute);
};
) =>
changePassword(password)
.then(redirectToAppHome)
.catch((e: unknown) => {
log.error("Could not change password", e);
setFieldError(
"confirm",
e instanceof Error &&
e.message == deriveKeyInsufficientMemoryErrorMessage
? t("password_generation_failed")
: t("generic_error"),
);
});
return (
<AccountsPageContents>

View File

@@ -1,3 +1,10 @@
import {
generateSRPSetupAttributes,
getSRPAttributes,
saveSRPAttributes,
updateSRPAndKeyAttributes,
type UpdatedKeyAttr,
} from "ente-accounts/services/srp";
import {
decryptBox,
deriveInteractiveKey,
@@ -6,14 +13,20 @@ import {
generateKey,
generateKeyPair,
} from "ente-base/crypto";
import { isDevBuild } from "ente-base/env";
import {
authenticatedRequestHeaders,
ensureOk,
publicRequestHeaders,
} from "ente-base/http";
import { apiURL } from "ente-base/origins";
import {
ensureMasterKeyFromSession,
saveMasterKeyInSessionAndSafeStore,
} from "ente-base/session";
import { getAuthToken } from "ente-base/token";
import { getData, setLSUser } from "ente-shared/storage/localStorage";
import { ensure } from "ente-utils/ensure";
import { nullToUndefined } from "ente-utils/transform";
import { z } from "zod/v4";
import { getUserRecoveryKey, recoveryKeyFromMnemonic } from "./recovery-key";
@@ -656,13 +669,22 @@ export const generateAndSaveInteractiveKeyAttributes = async (
};
/**
* Change the email associated with the user's account on remote.
* Change the email associated with the user's account (both locally and on
* remote)
*
* @param email The new email.
*
* @param ott The verification code that was sent to the new email.
*/
export const changeEmail = async (email: string, ott: string) =>
export const changeEmail = async (email: string, ott: string) => {
await postChangeEmail(email, ott);
await setLSUser({ ...getData("user"), email });
};
/**
* Change the email associated with the user's account on remote.
*/
const postChangeEmail = async (email: string, ott: string) =>
ensureOk(
await fetch(await apiURL("/users/change-email"), {
method: "POST",
@@ -671,6 +693,60 @@ export const changeEmail = async (email: string, ott: string) =>
}),
);
/**
* Change the user's password on both remote and locally.
*
* @param password The new password.
*/
export const changePassword = async (password: string) => {
const user = ensureLocalUser();
const masterKey = await ensureMasterKeyFromSession();
const keyAttributes = ensureSavedKeyAttributes();
// Generate new KEK.
const {
key: kek,
salt: kekSalt,
opsLimit,
memLimit,
} = await deriveSensitiveKey(password);
// Generate new key attributes.
const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } =
await encryptBox(masterKey, kek);
const updatedKeyAttr: UpdatedKeyAttr = {
encryptedKey,
keyDecryptionNonce,
kekSalt,
opsLimit,
memLimit,
};
// Update SRP and key attributes on remote.
await updateSRPAndKeyAttributes(
await generateSRPSetupAttributes(kek),
updatedKeyAttr,
);
// Update SRP attributes locally.
const srpAttributes = await getSRPAttributes(user.email);
saveSRPAttributes(ensure(srpAttributes));
// Update key attributes locally, generating a new interactive kek while
// we're at it.
await generateAndSaveInteractiveKeyAttributes(
password,
{ ...keyAttributes, ...updatedKeyAttr },
masterKey,
);
// TODO(RE): This shouldn't be needed, remove me. As a soft remove,
// disabling it for dev builds. (tag: Migration)
if (!isDevBuild) {
await saveMasterKeyInSessionAndSafeStore(masterKey);
}
};
const TwoFactorSecret = z.object({
/**
* The 2FA secret code.

View File

@@ -8,6 +8,28 @@ export const ensurePrecondition = (v: unknown): void => {
if (!v) throw new Error("Precondition failed");
};
/**
* Throw an exception if the given value is `null` or `undefined`.
*
* This is different from TypeScript's built in null assertion operator `!` in
* that `ensure` involves a runtime check, and will throw if the given value is
* null-ish. On the other hand the TypeScript null assertion is only an
* indication to the type system and does not involve any runtime checks.
*
* However, still it is preferable to use the TypeScript build in null assertion
* since the stack traces are more informative. The stack trace is not at the
* point of the assertion, but later at the point of the use, so it is not
* _directly_ pointing at the issue, but usually it is not hard to backtrace.
*
* Still, in rare cases we might want to, well, ensure that a undefined value
* doesn't sneak into the machinery. So this.
*/
export const ensure = <T>(v: T | null | undefined): T => {
if (v === null) throw new Error("Required value was null");
if (v === undefined) throw new Error("Required value was undefined");
return v;
};
/**
* Throw an exception if the given value is not a string.
*/