Update
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
saveKeyAttributes,
|
||||
saveSRPAttributes,
|
||||
setLSUser,
|
||||
updateSavedLocalUser,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import {
|
||||
openPasskeyVerificationURL,
|
||||
@@ -212,12 +213,7 @@ const Page: React.FC = () => {
|
||||
|
||||
if (passkeySessionID) {
|
||||
await stashKeyEncryptionKeyInSessionStore(kek);
|
||||
const user = getData("user");
|
||||
await setLSUser({
|
||||
...user,
|
||||
passkeySessionID,
|
||||
isTwoFactorEnabled: true,
|
||||
});
|
||||
updateSavedLocalUser({ passkeySessionID });
|
||||
stashRedirect("/");
|
||||
const url = passkeyVerificationRedirectURL(
|
||||
accountsUrl!,
|
||||
@@ -228,11 +224,9 @@ const Page: React.FC = () => {
|
||||
throw new Error(twoFactorEnabledErrorMessage);
|
||||
} else if (twoFactorSessionID) {
|
||||
await stashKeyEncryptionKeyInSessionStore(kek);
|
||||
const user = getData("user");
|
||||
await setLSUser({
|
||||
...user,
|
||||
twoFactorSessionID,
|
||||
updateSavedLocalUser({
|
||||
isTwoFactorEnabled: true,
|
||||
twoFactorSessionID,
|
||||
});
|
||||
void router.push("/two-factor/verify");
|
||||
throw new Error(twoFactorEnabledErrorMessage);
|
||||
@@ -243,7 +237,9 @@ const Page: React.FC = () => {
|
||||
token,
|
||||
encryptedToken,
|
||||
id,
|
||||
isTwoFactorEnabled: false,
|
||||
isTwoFactorEnabled: undefined,
|
||||
twoFactorSessionID: undefined,
|
||||
passkeySessionID: undefined,
|
||||
});
|
||||
if (keyAttributes) saveKeyAttributes(keyAttributes);
|
||||
return keyAttributes;
|
||||
|
||||
@@ -49,6 +49,8 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||
*
|
||||
* - Redirects to the passkey app once email verification is complete if the
|
||||
* user has setup an additional passkey that also needs to be verified.
|
||||
* Before redirecting, it sets the `inflightPasskeySessionID` in session
|
||||
* storage.
|
||||
*
|
||||
* - "/credentials" - A page that allows the user to enter their password to
|
||||
* authenticate (initial login) or reauthenticate (new web app tab)
|
||||
@@ -79,11 +81,41 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||
*
|
||||
* - Redirects to "/change-password" once the recovery key is verified.
|
||||
*
|
||||
* - "/change-password" - A page that allows the user to reset their password.
|
||||
* - "/change-password" - A page that allows the user to reset their password.
|
||||
*
|
||||
* - Redirects to "/" if there is no `email` present in the saved partial
|
||||
* user, and after successfully changing the password.
|
||||
*
|
||||
* - "/two-factor/verify" - A page that allows the user to verify their TOTP
|
||||
* based second factor.
|
||||
*
|
||||
* - Redirects to "/" if there is no `email` or `twoFactorSessionID` in the
|
||||
* saved partial local user.
|
||||
*
|
||||
* - Redirects to "/credentials" if there `isTwoFactorEnabled` is not `true`
|
||||
* and either of `encryptedToken` or `token` is present in the saved partial
|
||||
* local user.
|
||||
*
|
||||
* - "/passkeys/finish" - A page that the accounts app hands off control back to
|
||||
* us (the calling app) to continue the rest of the authentication.
|
||||
*
|
||||
* - Redirects to "/" if there is no matching `inflightPasskeySessionID` in
|
||||
* session storage.
|
||||
*
|
||||
* - Redirects to "/credentials" otherwise.
|
||||
*
|
||||
* - "/two-factor/recover" and "/passkeys/recover" - Pages that allow the user
|
||||
* to reset or bypass their second factor if they possess their recovery key.
|
||||
* Both pages work similarly, except the second factor they act on.
|
||||
*
|
||||
* - Redirects to "/" if there is no `email` in the saved partial local user,
|
||||
* or either of `twoFactorSessionID` and `twoFactorSessionID` is set.
|
||||
*
|
||||
* - Redirects to "/generate" if there is an `encryptedToken` or `token` in
|
||||
* the saved partial local user (TODO: Why?).
|
||||
*
|
||||
* - Redirects to "/credentials" after recovery.
|
||||
*
|
||||
*/
|
||||
const Page: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
AccountsPageFooter,
|
||||
AccountsPageTitle,
|
||||
} from "ente-accounts/components/layouts/centered-paper";
|
||||
import { getData } from "ente-accounts/services/accounts-db";
|
||||
import { savedPartialLocalUser } from "ente-accounts/services/accounts-db";
|
||||
import {
|
||||
recoverTwoFactor,
|
||||
recoverTwoFactorFinish,
|
||||
@@ -64,14 +64,14 @@ const Page: React.FC<RecoverPageProps> = ({ twoFactorType }) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const user = getData("user");
|
||||
const sessionID = user.passkeySessionID || user.twoFactorSessionID;
|
||||
const user = savedPartialLocalUser();
|
||||
const sessionID =
|
||||
twoFactorType == "passkey"
|
||||
? user?.passkeySessionID
|
||||
: user?.twoFactorSessionID;
|
||||
if (!user?.email || !sessionID) {
|
||||
void router.push("/");
|
||||
} else if (
|
||||
!user.isTwoFactorEnabled &&
|
||||
(user.encryptedToken || user.token)
|
||||
) {
|
||||
} else if (user.encryptedToken || user.token) {
|
||||
void router.push("/generate");
|
||||
} else {
|
||||
setSessionID(sessionID);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Verify2FACodeForm } from "ente-accounts/components/Verify2FACodeForm";
|
||||
import {
|
||||
getData,
|
||||
savedPartialLocalUser,
|
||||
saveKeyAttributes,
|
||||
setLSUser,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import type { PartialLocalUser } from "ente-accounts/services/user";
|
||||
import { verifyTwoFactor } from "ente-accounts/services/user";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import { isHTTPErrorWithStatus } from "ente-base/http";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
AccountsPageContents,
|
||||
AccountsPageFooter,
|
||||
@@ -19,15 +19,18 @@ import {
|
||||
} from "../../components/layouts/centered-paper";
|
||||
import { unstashRedirect } from "../../services/redirect";
|
||||
|
||||
/**
|
||||
* A page that allows the user to verify their TOTP based second factor.
|
||||
*/
|
||||
const Page: React.FC = () => {
|
||||
const { logout } = useBaseContext();
|
||||
|
||||
const [sessionID, setSessionID] = useState("");
|
||||
const [twoFactorSessionID, setTwoFactorSessionID] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const user: PartialLocalUser = getData("user");
|
||||
const user = savedPartialLocalUser();
|
||||
if (!user?.email || !user.twoFactorSessionID) {
|
||||
void router.push("/");
|
||||
} else if (
|
||||
@@ -36,37 +39,38 @@ const Page: React.FC = () => {
|
||||
) {
|
||||
void router.push("/credentials");
|
||||
} else {
|
||||
setSessionID(user.twoFactorSessionID);
|
||||
setTwoFactorSessionID(user.twoFactorSessionID);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const handleSubmit = async (otp: string) => {
|
||||
try {
|
||||
const { keyAttributes, encryptedToken, id } = await verifyTwoFactor(
|
||||
otp,
|
||||
sessionID,
|
||||
);
|
||||
await setLSUser({
|
||||
...getData("user"),
|
||||
id,
|
||||
// TODO: [Note: empty token?]
|
||||
//
|
||||
// The original code was parsing an token which is never going
|
||||
// to be present in the response, so effectively was always
|
||||
// setting token to undefined. So this works, but is it needed?
|
||||
token: undefined,
|
||||
encryptedToken,
|
||||
});
|
||||
saveKeyAttributes(keyAttributes);
|
||||
await router.push(unstashRedirect() ?? "/credentials");
|
||||
} catch (e) {
|
||||
if (isHTTPErrorWithStatus(e, 404)) {
|
||||
logout();
|
||||
} else {
|
||||
throw e;
|
||||
const handleSubmit = useCallback(
|
||||
async (otp: string) => {
|
||||
try {
|
||||
const { keyAttributes, encryptedToken, id } =
|
||||
await verifyTwoFactor(otp, twoFactorSessionID);
|
||||
await setLSUser({
|
||||
...getData("user"),
|
||||
id,
|
||||
// TODO: [Note: empty token?]
|
||||
//
|
||||
// The original code was parsing an token which is never going
|
||||
// to be present in the response, so effectively was always
|
||||
// setting token to undefined. So this works, but is it needed?
|
||||
token: undefined,
|
||||
encryptedToken,
|
||||
});
|
||||
saveKeyAttributes(keyAttributes);
|
||||
await router.push(unstashRedirect() ?? "/credentials");
|
||||
} catch (e) {
|
||||
if (isHTTPErrorWithStatus(e, 404)) {
|
||||
logout();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[logout, router, twoFactorSessionID],
|
||||
);
|
||||
|
||||
return (
|
||||
<AccountsPageContents>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { VerifyingPasskey } from "ente-accounts/components/LoginComponents";
|
||||
import { SecondFactorChoice } from "ente-accounts/components/SecondFactorChoice";
|
||||
import { useSecondFactorChoiceIfNeeded } from "ente-accounts/components/utils/second-factor-choice";
|
||||
import {
|
||||
getData,
|
||||
savedKeyAttributes,
|
||||
savedOriginalKeyAttributes,
|
||||
savedPartialLocalUser,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
setLSUser,
|
||||
unstashAfterUseSRPSetupAttributes,
|
||||
unstashReferralSource,
|
||||
updateSavedLocalUser,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import {
|
||||
openPasskeyVerificationURL,
|
||||
@@ -101,12 +101,7 @@ const Page: React.FC = () => {
|
||||
await verifyEmail(email, ott, cleanedReferral),
|
||||
);
|
||||
if (passkeySessionID) {
|
||||
const user = getData("user");
|
||||
await setLSUser({
|
||||
...user,
|
||||
passkeySessionID,
|
||||
isTwoFactorEnabled: true,
|
||||
});
|
||||
updateSavedLocalUser({ passkeySessionID });
|
||||
saveIsFirstLogin();
|
||||
const url = passkeyVerificationRedirectURL(
|
||||
accountsUrl!,
|
||||
@@ -115,10 +110,9 @@ const Page: React.FC = () => {
|
||||
setPasskeyVerificationData({ passkeySessionID, url });
|
||||
openPasskeyVerificationURL({ passkeySessionID, url });
|
||||
} else if (twoFactorSessionID) {
|
||||
await setLSUser({
|
||||
email,
|
||||
twoFactorSessionID,
|
||||
updateSavedLocalUser({
|
||||
isTwoFactorEnabled: true,
|
||||
twoFactorSessionID,
|
||||
});
|
||||
saveIsFirstLogin();
|
||||
void router.push("/two-factor/verify");
|
||||
@@ -128,7 +122,9 @@ const Page: React.FC = () => {
|
||||
token,
|
||||
encryptedToken,
|
||||
id,
|
||||
isTwoFactorEnabled: false,
|
||||
isTwoFactorEnabled: undefined,
|
||||
twoFactorSessionID: undefined,
|
||||
passkeySessionID: undefined,
|
||||
});
|
||||
if (keyAttributes) {
|
||||
saveKeyAttributes(keyAttributes);
|
||||
|
||||
@@ -58,6 +58,9 @@ import {
|
||||
* also get filled in. Once they verify their TOTP based second factor, their
|
||||
* {@link id} and {@link encryptedToken} will also get filled in.
|
||||
*
|
||||
* - If they have a passkey set as a second factor set, then after verifying
|
||||
* their password the {@link passkeySessionID} will be set.
|
||||
*
|
||||
* - As the login or signup sequence completes, a {@link token} obtained from
|
||||
* the {@link encryptedToken} will be written out, and the
|
||||
* {@link encryptedToken} cleared since it is not needed anymore.
|
||||
@@ -105,6 +108,7 @@ const LocalUser = z.object({
|
||||
id: z.number(),
|
||||
email: z.string(),
|
||||
token: z.string(),
|
||||
isTwoFactorEnabled: z.boolean().nullish().transform(nullToUndefined),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -127,13 +131,33 @@ export const savedPartialLocalUser = (): PartialLocalUser | undefined => {
|
||||
*
|
||||
* See: [Note: Partial local user].
|
||||
*
|
||||
* This method replaces the existing data. Use {@link updatePartialLocalUser} to
|
||||
* update selected fields while keeping the other fields as it is.
|
||||
*
|
||||
* TODO: WARNING: This does not update the KV token. The idea is to gradually
|
||||
* move over uses of setLSUser to this while explicitly setting the KV token
|
||||
* where needed.
|
||||
*/
|
||||
export const savePartialLocalUser = (partialLocalUser: Partial<LocalUser>) =>
|
||||
export const savePartialLocalUser = (partialLocalUser: PartialLocalUser) =>
|
||||
localStorage.setItem("user", JSON.stringify(partialLocalUser));
|
||||
|
||||
/**
|
||||
* Partially update the saved user data.
|
||||
*
|
||||
* This is a delta variant of {@link savePartialLocalUser}, which replaces the
|
||||
* entire saved object, while this function spreads the provided {@link updates}
|
||||
* onto the currently saved value.
|
||||
*
|
||||
* @param updates A subset of {@link PartialLocalUser} fields that we'd like to
|
||||
* update. The other fields, if any, remain unchanged.
|
||||
*
|
||||
* TODO: WARNING: This does not update the KV token. The idea is to gradually
|
||||
* move over uses of setLSUser to this while explicitly setting the KV token
|
||||
* where needed.
|
||||
*/
|
||||
export const updateSavedLocalUser = (updates: Partial<PartialLocalUser>) =>
|
||||
savePartialLocalUser({ ...savedPartialLocalUser(), ...updates });
|
||||
|
||||
/**
|
||||
* Return data about the logged-in user, if someone is indeed logged in.
|
||||
* Otherwise return `undefined`.
|
||||
@@ -143,7 +167,8 @@ export const savePartialLocalUser = (partialLocalUser: Partial<LocalUser>) =>
|
||||
* not accessible to web workers.
|
||||
*
|
||||
* There is no setter corresponding to this function since this is only a view
|
||||
* on data saved using {@link savePartialLocalUser}.
|
||||
* on data saved using {@link savePartialLocalUser} or
|
||||
* {@link updateSavedLocalUser}.
|
||||
*
|
||||
* See: [Note: Partial local user] for more about the whole shebang.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
saveKeyAttributes,
|
||||
saveSRPAttributes,
|
||||
setLSUser,
|
||||
updateSavedLocalUser,
|
||||
type PartialLocalUser,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import {
|
||||
@@ -70,6 +71,10 @@ export interface LocalUser {
|
||||
* in IndexedDB and thus can also be used in web workers.
|
||||
*/
|
||||
token: string;
|
||||
/**
|
||||
* `true` if the TOTP based second factor is enabled for the user.
|
||||
*/
|
||||
isTwoFactorEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -664,7 +669,7 @@ export const generateAndSaveInteractiveKeyAttributes = async (
|
||||
*/
|
||||
export const changeEmail = async (email: string, ott: string) => {
|
||||
await postChangeEmail(email, ott);
|
||||
await setLSUser({ ...getData("user"), email });
|
||||
updateSavedLocalUser({ email });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -784,7 +789,7 @@ export const setupTwoFactorFinish = async (
|
||||
encryptedTwoFactorSecret: box.encryptedData,
|
||||
twoFactorSecretDecryptionNonce: box.nonce,
|
||||
});
|
||||
await setLSUser({ ...getData("user"), isTwoFactorEnabled: true });
|
||||
updateSavedLocalUser({ isTwoFactorEnabled: true });
|
||||
};
|
||||
|
||||
interface EnableTwoFactorRequest {
|
||||
@@ -956,7 +961,7 @@ export const recoverTwoFactorFinish = async (
|
||||
await setLSUser({
|
||||
...getData("user"),
|
||||
id,
|
||||
isTwoFactorEnabled: false,
|
||||
isTwoFactorEnabled: undefined,
|
||||
encryptedToken,
|
||||
token: undefined,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { getData, setLSUser } from "ente-accounts/services/accounts-db";
|
||||
import {
|
||||
savedPartialLocalUser,
|
||||
updateSavedLocalUser,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import {
|
||||
RowButton,
|
||||
RowButtonGroup,
|
||||
@@ -24,12 +27,9 @@ export const TwoFactorSettings: React.FC<
|
||||
const [isTwoFactorEnabled, setIsTwoFactorEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const isTwoFactorEnabled =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
getData("user").isTwoFactorEnabled ?? false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
setIsTwoFactorEnabled(isTwoFactorEnabled);
|
||||
if (savedPartialLocalUser()?.isTwoFactorEnabled) {
|
||||
setIsTwoFactorEnabled(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,11 +37,7 @@ export const TwoFactorSettings: React.FC<
|
||||
void (async () => {
|
||||
const isEnabled = await get2FAStatus();
|
||||
setIsTwoFactorEnabled(isEnabled);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
await setLSUser({
|
||||
...getData("user"),
|
||||
isTwoFactorEnabled: isEnabled,
|
||||
});
|
||||
updateSavedLocalUser({ isTwoFactorEnabled: isEnabled });
|
||||
})();
|
||||
}, [open]);
|
||||
|
||||
@@ -112,8 +108,7 @@ const ManageDrawerContents: React.FC<ContentsProps> = ({ onRootClose }) => {
|
||||
|
||||
const disable = async () => {
|
||||
await disable2FA();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
await setLSUser({ ...getData("user"), isTwoFactorEnabled: false });
|
||||
updateSavedLocalUser({ isTwoFactorEnabled: undefined });
|
||||
onRootClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getData, setLSUser } from "ente-accounts/services/accounts-db";
|
||||
import { updateSavedLocalUser } from "ente-accounts/services/accounts-db";
|
||||
import { ensureLocalUser } from "ente-accounts/services/user";
|
||||
import { isDesktop } from "ente-base/app";
|
||||
import { authenticatedRequestHeaders, ensureOk } from "ente-base/http";
|
||||
@@ -241,10 +241,10 @@ export const pullUserDetails = async () => {
|
||||
|
||||
// Update the email for the local storage user if needed (the user might've
|
||||
// changed their email on a different client).
|
||||
if (ensureLocalUser().email != userDetails.email) {
|
||||
const { email } = userDetails;
|
||||
if (ensureLocalUser().email != email) {
|
||||
log.info("Updating user email to match fetched user details");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
await setLSUser({ ...getData("user"), email: userDetails.email });
|
||||
updateSavedLocalUser({ email });
|
||||
}
|
||||
|
||||
// The gallery listens for updates to userDetails, so a special case, do a
|
||||
|
||||
Reference in New Issue
Block a user