Redo logout

This commit is contained in:
Manav Rathi 2024-05-15 11:44:00 +05:30
parent 76a09b1473
commit 0bcc6e3f3f
No known key found for this signature in database
21 changed files with 151 additions and 116 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ import { APPS } from "@ente/shared/apps/constants";
import { CenteredFlex } from "@ente/shared/components/Container"; import { CenteredFlex } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner"; import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
import { CustomError } from "@ente/shared/error"; import { CustomError } from "@ente/shared/error";
import { useFileInput } from "@ente/shared/hooks/useFileInput"; import { useFileInput } from "@ente/shared/hooks/useFileInput";
import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded"; import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
@ -93,11 +94,7 @@ import { getLocalFiles, syncFiles } from "services/fileService";
import locationSearchService from "services/locationSearchService"; import locationSearchService from "services/locationSearchService";
import { getLocalTrashedFiles, syncTrash } from "services/trashService"; import { getLocalTrashedFiles, syncTrash } from "services/trashService";
import uploadManager from "services/upload/uploadManager"; import uploadManager from "services/upload/uploadManager";
import { import { isTokenValid, syncMapEnabled } from "services/userService";
isTokenValid,
syncMapEnabled,
validateKey,
} from "services/userService";
import { Collection, CollectionSummaries } from "types/collection"; import { Collection, CollectionSummaries } from "types/collection";
import { EnteFile } from "types/file"; import { EnteFile } from "types/file";
import { import {
@ -249,8 +246,13 @@ export default function Gallery() {
const [tempHiddenFileIds, setTempHiddenFileIds] = useState<Set<number>>( const [tempHiddenFileIds, setTempHiddenFileIds] = useState<Set<number>>(
new Set<number>(), new Set<number>(),
); );
const { startLoading, finishLoading, setDialogMessage, ...appContext } = const {
useContext(AppContext); startLoading,
finishLoading,
setDialogMessage,
logout,
...appContext
} = useContext(AppContext);
const [collectionSummaries, setCollectionSummaries] = const [collectionSummaries, setCollectionSummaries] =
useState<CollectionSummaries>(); useState<CollectionSummaries>();
const [hiddenCollectionSummaries, setHiddenCollectionSummaries] = const [hiddenCollectionSummaries, setHiddenCollectionSummaries] =
@ -319,6 +321,19 @@ export default function Gallery() {
const [isClipSearchResult, setIsClipSearchResult] = const [isClipSearchResult, setIsClipSearchResult] =
useState<boolean>(false); 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(() => { useEffect(() => {
appContext.showNavBar(true); appContext.showNavBar(true);
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);

View file

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

View file

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

View file

@ -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);
}
};

View file

@ -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 () => { export const getFaceSearchEnabledStatus = async () => {
try { try {
const token = getToken(); const token = getToken();

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import log from "@/next/log";
import { recoverTwoFactor, removeTwoFactor } from "@ente/accounts/api/user"; import { recoverTwoFactor, removeTwoFactor } from "@ente/accounts/api/user";
import { PAGES } from "@ente/accounts/constants/pages"; import { PAGES } from "@ente/accounts/constants/pages";
import { TwoFactorType } from "@ente/accounts/constants/twofactor"; 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 { PageProps } from "@ente/shared/apps/types";
import { VerticallyCentered } from "@ente/shared/components/Container"; import { VerticallyCentered } from "@ente/shared/components/Container";
import { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types"; import { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";

View file

@ -3,7 +3,7 @@ import VerifyTwoFactor, {
VerifyTwoFactorCallback, VerifyTwoFactorCallback,
} from "@ente/accounts/components/two-factor/VerifyForm"; } from "@ente/accounts/components/two-factor/VerifyForm";
import { PAGES } from "@ente/accounts/constants/pages"; 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 type { PageProps } from "@ente/shared/apps/types";
import { VerticallyCentered } from "@ente/shared/components/Container"; import { VerticallyCentered } from "@ente/shared/components/Container";
import FormPaper from "@ente/shared/components/Form/FormPaper"; import FormPaper from "@ente/shared/components/Form/FormPaper";

View file

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

View file

@ -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);
}
};

View file

@ -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);
};

View file

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

View file

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

View file

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