Manav Rathi 1 سال پیش
والد
کامیت
0bcc6e3f3f

+ 2 - 3
web/apps/auth/src/components/Navbar.tsx

@@ -1,4 +1,3 @@
-import { logoutUser } from "@ente/accounts/services/user";
 import { HorizontalFlex } from "@ente/shared/components/Container";
 import { EnteLogo } from "@ente/shared/components/EnteLogo";
 import NavbarBase from "@ente/shared/components/Navbar/base";
@@ -11,7 +10,7 @@ import { AppContext } from "pages/_app";
 import React from "react";
 
 export default function AuthNavbar() {
-    const { isMobile } = React.useContext(AppContext);
+    const { isMobile, logout } = React.useContext(AppContext);
     return (
         <NavbarBase isMobile={isMobile}>
             <HorizontalFlex flex={1} justifyContent={"center"}>
@@ -25,7 +24,7 @@ export default function AuthNavbar() {
                     <OverflowMenuOption
                         color="critical"
                         startIcon={<LogoutOutlined />}
-                        onClick={logoutUser}
+                        onClick={logout}
                     >
                         {t("LOGOUT")}
                     </OverflowMenuOption>

+ 8 - 0
web/apps/auth/src/pages/_app.tsx

@@ -4,6 +4,7 @@ import {
     logStartupBanner,
     logUnhandledErrorsAndRejections,
 } from "@/next/log-web";
+import { accountLogout } from "@ente/accounts/services/logout";
 import {
     APPS,
     APP_TITLES,
@@ -44,6 +45,7 @@ type AppContextType = {
     setThemeColor: SetTheme;
     somethingWentWrong: () => void;
     setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
+    logout: () => void;
 };
 
 export const AppContext = createContext<AppContextType>(null);
@@ -128,6 +130,11 @@ export default function App({ Component, pageProps }: AppProps) {
             content: t("UNKNOWN_ERROR"),
         });
 
+    const logout = async () => {
+        await accountLogout();
+        router.push(PAGES.ROOT);
+    };
+
     const title = isI18nReady
         ? t("TITLE", { context: APPS.AUTH })
         : APP_TITLES.get(APPS.AUTH);
@@ -162,6 +169,7 @@ export default function App({ Component, pageProps }: AppProps) {
                         setThemeColor,
                         somethingWentWrong,
                         setDialogBoxAttributesV2,
+                        logout,
                     }}
                 >
                     {(loading || !isI18nReady) && (

+ 3 - 3
web/apps/photos/src/components/DeleteAccountModal.tsx

@@ -1,5 +1,4 @@
 import log from "@/next/log";
-import { logoutUser } from "@ente/accounts/services/user";
 import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
 import EnteButton from "@ente/shared/components/EnteButton";
 import { DELETE_ACCOUNT_EMAIL } from "@ente/shared/constants/urls";
@@ -43,7 +42,8 @@ const getReasonOptions = (): DropdownOption<DELETE_REASON>[] => {
 };
 
 const DeleteAccountModal = ({ open, onClose }: Iprops) => {
-    const { setDialogBoxAttributesV2, isMobile } = useContext(AppContext);
+    const { setDialogBoxAttributesV2, isMobile, logout } =
+        useContext(AppContext);
     const { authenticateUser } = useContext(GalleryContext);
     const [loading, setLoading] = useState(false);
     const deleteAccountChallenge = useRef<string>();
@@ -145,7 +145,7 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => {
             );
             const { reason, feedback } = reasonAndFeedbackRef.current;
             await deleteAccount(decryptedChallenge, reason, feedback);
-            logoutUser();
+            logout();
         } catch (e) {
             log.error("solveChallengeAndDeleteAccount failed", e);
             somethingWentWrong();

+ 4 - 6
web/apps/photos/src/components/Sidebar/ExitSection.tsx

@@ -1,13 +1,11 @@
-import { t } from "i18next";
-import { useContext, useState } from "react";
-
-import { logoutUser } from "@ente/accounts/services/user";
 import DeleteAccountModal from "components/DeleteAccountModal";
 import { EnteMenuItem } from "components/Menu/EnteMenuItem";
+import { t } from "i18next";
 import { AppContext } from "pages/_app";
+import { useContext, useState } from "react";
 
 export default function ExitSection() {
-    const { setDialogMessage } = useContext(AppContext);
+    const { setDialogMessage, logout } = useContext(AppContext);
 
     const [deleteAccountModalView, setDeleteAccountModalView] = useState(false);
 
@@ -19,7 +17,7 @@ export default function ExitSection() {
             title: t("LOGOUT_MESSAGE"),
             proceed: {
                 text: t("LOGOUT"),
-                action: logoutUser,
+                action: logout,
                 variant: "critical",
             },
             close: { text: t("CANCEL") },

+ 8 - 0
web/apps/photos/src/pages/_app.tsx

@@ -53,6 +53,7 @@ import { createContext, useEffect, useRef, useState } from "react";
 import LoadingBar from "react-top-loading-bar";
 import DownloadManager from "services/download";
 import exportService, { resumeExportsIfNeeded } from "services/export";
+import { photosLogout } from "services/logout";
 import {
     getMLSearchConfig,
     updateMLSearchConfig,
@@ -100,6 +101,7 @@ type AppContextType = {
     setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
     isCFProxyDisabled: boolean;
     setIsCFProxyDisabled: (disabled: boolean) => void;
+    logout: () => Promise<void>;
 };
 
 export const AppContext = createContext<AppContextType>(null);
@@ -336,6 +338,11 @@ export default function App({ Component, pageProps }: AppProps) {
             content: t("UNKNOWN_ERROR"),
         });
 
+    const logout = async () => {
+        await photosLogout();
+        router.push(PAGES.ROOT);
+    };
+
     const title = isI18nReady
         ? t("TITLE", { context: APPS.PHOTOS })
         : APP_TITLES.get(APPS.PHOTOS);
@@ -394,6 +401,7 @@ export default function App({ Component, pageProps }: AppProps) {
                         updateMapEnabled,
                         isCFProxyDisabled,
                         setIsCFProxyDisabled,
+                        logout,
                     }}
                 >
                     {(loading || !isI18nReady) && (

+ 22 - 7
web/apps/photos/src/pages/gallery/index.tsx

@@ -3,6 +3,7 @@ import { APPS } from "@ente/shared/apps/constants";
 import { CenteredFlex } from "@ente/shared/components/Container";
 import EnteSpinner from "@ente/shared/components/EnteSpinner";
 import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
+import { getRecoveryKey } from "@ente/shared/crypto/helpers";
 import { CustomError } from "@ente/shared/error";
 import { useFileInput } from "@ente/shared/hooks/useFileInput";
 import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
@@ -93,11 +94,7 @@ import { getLocalFiles, syncFiles } from "services/fileService";
 import locationSearchService from "services/locationSearchService";
 import { getLocalTrashedFiles, syncTrash } from "services/trashService";
 import uploadManager from "services/upload/uploadManager";
-import {
-    isTokenValid,
-    syncMapEnabled,
-    validateKey,
-} from "services/userService";
+import { isTokenValid, syncMapEnabled } from "services/userService";
 import { Collection, CollectionSummaries } from "types/collection";
 import { EnteFile } from "types/file";
 import {
@@ -249,8 +246,13 @@ export default function Gallery() {
     const [tempHiddenFileIds, setTempHiddenFileIds] = useState<Set<number>>(
         new Set<number>(),
     );
-    const { startLoading, finishLoading, setDialogMessage, ...appContext } =
-        useContext(AppContext);
+    const {
+        startLoading,
+        finishLoading,
+        setDialogMessage,
+        logout,
+        ...appContext
+    } = useContext(AppContext);
     const [collectionSummaries, setCollectionSummaries] =
         useState<CollectionSummaries>();
     const [hiddenCollectionSummaries, setHiddenCollectionSummaries] =
@@ -319,6 +321,19 @@ export default function Gallery() {
     const [isClipSearchResult, setIsClipSearchResult] =
         useState<boolean>(false);
 
+    // Ensure that the keys in local storage are not malformed by verifying that
+    // the recoveryKey can be decrypted with the masterKey.
+    // Note: This is not bullet-proof.
+    const validateKey = async () => {
+        try {
+            await getRecoveryKey();
+            return true;
+        } catch (e) {
+            await logout();
+            return false;
+        }
+    };
+
     useEffect(() => {
         appContext.showNavBar(true);
         const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);

+ 1 - 2
web/apps/photos/src/pages/shared-albums/index.tsx

@@ -1,5 +1,4 @@
 import log from "@/next/log";
-import { logoutUser } from "@ente/accounts/services/user";
 import { APPS } from "@ente/shared/apps/constants";
 import {
     CenteredFlex,
@@ -185,7 +184,7 @@ export default function PublicCollectionGallery() {
             nonClosable: true,
             proceed: {
                 text: t("LOGIN"),
-                action: logoutUser,
+                action: () => router.push(PAGES.ROOT),
                 variant: "accent",
             },
         });

+ 3 - 2
web/apps/photos/src/services/clip-service.ts

@@ -87,14 +87,15 @@ class CLIPService {
         return isElectron();
     };
 
-    private logoutHandler = async () => {
+    async logout() {
         if (this.embeddingExtractionInProgress) {
             this.embeddingExtractionInProgress.abort();
         }
         if (this.onFileUploadedHandler) {
             await this.removeOnFileUploadListener();
         }
-    };
+    }
+
 
     setupOnFileUploadListener = async () => {
         try {

+ 38 - 0
web/apps/photos/src/services/logout.ts

@@ -0,0 +1,38 @@
+import log from "@/next/log";
+import { accountLogout } from "@ente/accounts/services/logout";
+import { Events, eventBus } from "@ente/shared/events";
+
+/**
+ * Logout sequence for the photos app.
+ *
+ * This function is guaranteed not to throw any errors.
+ *
+ * See: [Note: Do not throw during logout].
+ */
+export const photosLogout = async () => {
+    await accountLogout();
+
+    const electron = globalThis.electron;
+    if (electron) {
+        try {
+            await electron.watch.reset();
+        } catch (e) {
+            log.error("Ignoring error when resetting native folder watches", e);
+        }
+        try {
+            await electron.clearConvertToMP4Results();
+        } catch (e) {
+            log.error("Ignoring error when clearing convert-to-mp4 results", e);
+        }
+        try {
+            await electron.clearStores();
+        } catch (e) {
+            log.error("Ignoring error when clearing native stores", e);
+        }
+    }
+    try {
+        eventBus.emit(Events.LOGOUT);
+    } catch (e) {
+        log.error("Ignoring error in event-bus logout handlers", e);
+    }
+};

+ 0 - 13
web/apps/photos/src/services/userService.ts

@@ -233,19 +233,6 @@ export const deleteAccount = async (
     }
 };
 
-// Ensure that the keys in local storage are not malformed by verifying that the
-// recoveryKey can be decrypted with the masterKey.
-// Note: This is not bullet-proof.
-export const validateKey = async () => {
-    try {
-        await getRecoveryKey();
-        return true;
-    } catch (e) {
-        await logoutUser();
-        return false;
-    }
-};
-
 export const getFaceSearchEnabledStatus = async () => {
     try {
         const token = getToken();

+ 1 - 1
web/packages/accounts/api/user.ts

@@ -43,7 +43,7 @@ export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
         },
     );
 
-export const _logout = async () => {
+export const logout = async () => {
     try {
         const token = getToken();
         await HTTPService.post(`${ENDPOINT}/users/logout`, null, undefined, {

+ 1 - 1
web/packages/accounts/pages/credentials.tsx

@@ -45,12 +45,12 @@ import { useRouter } from "next/router";
 import { useEffect, useState } from "react";
 import { getSRPAttributes } from "../api/srp";
 import { PAGES } from "../constants/pages";
+import { logoutUser } from "../services/logout";
 import {
     configureSRP,
     generateSRPSetupAttributes,
     loginViaSRP,
 } from "../services/srp";
-import { logoutUser } from "../services/user";
 import { SRPAttributes } from "../types/srp";
 
 export default function Credentials({ appContext, appName }: PageProps) {

+ 1 - 1
web/packages/accounts/pages/generate.tsx

@@ -1,7 +1,7 @@
 import log from "@/next/log";
 import { putAttributes } from "@ente/accounts/api/user";
+import { logoutUser } from "@ente/accounts/services/logout";
 import { configureSRP } from "@ente/accounts/services/srp";
-import { logoutUser } from "@ente/accounts/services/user";
 import { generateKeyAndSRPAttributes } from "@ente/accounts/utils/srp";
 import {
     generateAndSaveIntermediateKeyAttributes,

+ 1 - 1
web/packages/accounts/pages/two-factor/recover.tsx

@@ -2,7 +2,7 @@ import log from "@/next/log";
 import { recoverTwoFactor, removeTwoFactor } from "@ente/accounts/api/user";
 import { PAGES } from "@ente/accounts/constants/pages";
 import { TwoFactorType } from "@ente/accounts/constants/twofactor";
-import { logoutUser } from "@ente/accounts/services/user";
+import { logoutUser } from "@ente/accounts/services/logout";
 import { PageProps } from "@ente/shared/apps/types";
 import { VerticallyCentered } from "@ente/shared/components/Container";
 import { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";

+ 1 - 1
web/packages/accounts/pages/two-factor/verify.tsx

@@ -3,7 +3,7 @@ import VerifyTwoFactor, {
     VerifyTwoFactorCallback,
 } from "@ente/accounts/components/two-factor/VerifyForm";
 import { PAGES } from "@ente/accounts/constants/pages";
-import { logoutUser } from "@ente/accounts/services/user";
+import { logoutUser } from "@ente/accounts/services/logout";
 import type { PageProps } from "@ente/shared/apps/types";
 import { VerticallyCentered } from "@ente/shared/components/Container";
 import FormPaper from "@ente/shared/components/Form/FormPaper";

+ 1 - 1
web/packages/accounts/pages/verify.tsx

@@ -29,8 +29,8 @@ import { HttpStatusCode } from "axios";
 import { useRouter } from "next/router";
 import { putAttributes, sendOtt, verifyOtt } from "../api/user";
 import { PAGES } from "../constants/pages";
+import { logoutUser } from "../services/logout";
 import { configureSRP } from "../services/srp";
-import { logoutUser } from "../services/user";
 import { SRPSetupAttributes } from "../types/srp";
 
 export default function VerifyPage({ appContext, appName }: PageProps) {

+ 50 - 0
web/packages/accounts/services/logout.ts

@@ -0,0 +1,50 @@
+import { clearCaches } from "@/next/blob-cache";
+import log from "@/next/log";
+import InMemoryStore from "@ente/shared/storage/InMemoryStore";
+import { clearFiles } from "@ente/shared/storage/localForage";
+import { clearData } from "@ente/shared/storage/localStorage";
+import { clearKeys } from "@ente/shared/storage/sessionStorage";
+import { logout as remoteLogout } from "../api/user";
+
+/**
+ * Logout sequence common to all apps that rely on the accounts package.
+ *
+ * [Note: Do not throw during logout]
+ *
+ * This function is guaranteed to not thrown any errors, and will try to
+ * independently complete all the steps in the sequence that can be completed.
+ * This allows the user to logout and start again even if somehow their account
+ * gets in an unexpected state.
+ */
+export const accountLogout = async () => {
+    try {
+        await remoteLogout();
+    } catch (e) {
+        log.error("Ignoring error during POST /users/logout", e);
+    }
+    try {
+        InMemoryStore.clear();
+    } catch (e) {
+        log.error("Ignoring error when clearing in-memory store", e);
+    }
+    try {
+        clearKeys();
+    } catch (e) {
+        log.error("Ignoring error when clearing keys", e);
+    }
+    try {
+        clearData();
+    } catch (e) {
+        log.error("Ignoring error when clearing data", e);
+    }
+    try {
+        await clearCaches();
+    } catch (e) {
+        log.error("Ignoring error when clearing caches", e);
+    }
+    try {
+        await clearFiles();
+    } catch (e) {
+        log.error("Ignoring error when clearing files", e);
+    }
+};

+ 0 - 67
web/packages/accounts/services/user.ts

@@ -1,67 +0,0 @@
-import { clearCaches } from "@/next/blob-cache";
-import log from "@/next/log";
-import { Events, eventBus } from "@ente/shared/events";
-import InMemoryStore from "@ente/shared/storage/InMemoryStore";
-import { clearFiles } from "@ente/shared/storage/localForage/helpers";
-import { clearData } from "@ente/shared/storage/localStorage";
-import { clearKeys } from "@ente/shared/storage/sessionStorage";
-import router from "next/router";
-import { _logout } from "../api/user";
-import { PAGES } from "../constants/pages";
-
-export const logoutUser = async () => {
-    try {
-        await _logout();
-    } catch (e) {
-        log.error("Ignoring error during POST /users/logout", e);
-    }
-    try {
-        InMemoryStore.clear();
-    } catch (e) {
-        log.error("Ignoring error when clearing in-memory store", e);
-    }
-    try {
-        clearKeys();
-    } catch (e) {
-        log.error("Ignoring error when clearing keys", e);
-    }
-    try {
-        clearData();
-    } catch (e) {
-        log.error("Ignoring error when clearing data", e);
-    }
-    try {
-        await clearCaches();
-    } catch (e) {
-        log.error("Ignoring error when clearing caches", e);
-    }
-    try {
-        await clearFiles();
-    } catch (e) {
-        log.error("Ignoring error when clearing files", e);
-    }
-    const electron = globalThis.electron;
-    if (electron) {
-        try {
-            await electron.watch.reset();
-        } catch (e) {
-            log.error("Ignoring error when resetting native folder watches", e);
-        }
-        try {
-            await electron.clearConvertToMP4Results();
-        } catch (e) {
-            log.error("Ignoring error when clearing convert-to-mp4 results", e);
-        }
-        try {
-            await electron.clearStores();
-        } catch (e) {
-            log.error("Ignoring error when clearing native stores", e);
-        }
-    }
-    try {
-        eventBus.emit(Events.LOGOUT);
-    } catch (e) {
-        log.error("Ignoring error in event-bus logout handlers", e);
-    }
-    router.push(PAGES.ROOT);
-};

+ 2 - 2
web/packages/next/types/ipc.ts

@@ -69,14 +69,14 @@ export interface Electron {
      * This is a coarse single shot cleanup, meant for use in clearing any
      * persisted Electron side state during logout.
      */
-    clearStores: () => void;
+    clearStores: () => Promise<void>;
 
     /**
      * Clear an state corresponding to in-flight convert-to-mp4 requests.
      *
      * This is meant for use during logout.
      */
-    clearConvertToMP4Results: () => void;
+    clearConvertToMP4Results: () => Promise<void>;
 
     /**
      * Return the previously saved encryption key from persistent safe storage.

+ 4 - 0
web/packages/shared/storage/localForage/index.ts → web/packages/shared/storage/localForage.ts

@@ -11,3 +11,7 @@ if (haveWindow()) {
 }
 
 export default localForage;
+
+export const clearFiles = async () => {
+    await localForage.clear();
+};

+ 0 - 5
web/packages/shared/storage/localForage/helpers.ts

@@ -1,5 +0,0 @@
-import localForage from ".";
-
-export const clearFiles = async () => {
-    await localForage.clear();
-};