[web] Systematize logout (#1729)

+ ML cleanup
This commit is contained in:
Manav Rathi 2024-05-15 13:58:17 +05:30 committed by GitHub
commit 69460418ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 313 additions and 339 deletions

View file

@ -17,7 +17,11 @@ import { existsSync } from "node:fs";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import {
attachFSWatchIPCHandlers,
attachIPCHandlers,
attachLogoutIPCHandler,
} from "./main/ipc";
import log, { initLogging } from "./main/log"; import log, { initLogging } from "./main/log";
import { createApplicationMenu, createTrayContextMenu } from "./main/menu"; import { createApplicationMenu, createTrayContextMenu } from "./main/menu";
import { setupAutoUpdater } from "./main/services/app-update"; import { setupAutoUpdater } from "./main/services/app-update";
@ -377,8 +381,12 @@ const main = () => {
void (async () => { void (async () => {
// Create window and prepare for the renderer. // Create window and prepare for the renderer.
mainWindow = createMainWindow(); mainWindow = createMainWindow();
// Setup IPC and streams.
const watcher = createWatcher(mainWindow);
attachIPCHandlers(); attachIPCHandlers();
attachFSWatchIPCHandlers(createWatcher(mainWindow)); attachFSWatchIPCHandlers(watcher);
attachLogoutIPCHandler(watcher);
registerStreamProtocol(); registerStreamProtocol();
// Configure the renderer's environment. // Configure the renderer's environment.

View file

@ -41,16 +41,13 @@ import {
fsWriteFile, fsWriteFile,
} from "./services/fs"; } from "./services/fs";
import { convertToJPEG, generateImageThumbnail } from "./services/image"; import { convertToJPEG, generateImageThumbnail } from "./services/image";
import { logout } from "./services/logout";
import { import {
clipImageEmbedding, clipImageEmbedding,
clipTextEmbeddingIfAvailable, clipTextEmbeddingIfAvailable,
} from "./services/ml-clip"; } from "./services/ml-clip";
import { detectFaces, faceEmbedding } from "./services/ml-face"; import { detectFaces, faceEmbedding } from "./services/ml-face";
import { import { encryptionKey, saveEncryptionKey } from "./services/store";
clearStores,
encryptionKey,
saveEncryptionKey,
} from "./services/store";
import { import {
clearPendingUploads, clearPendingUploads,
listZipItems, listZipItems,
@ -65,11 +62,9 @@ import {
watchFindFiles, watchFindFiles,
watchGet, watchGet,
watchRemove, watchRemove,
watchReset,
watchUpdateIgnoredFiles, watchUpdateIgnoredFiles,
watchUpdateSyncedFiles, watchUpdateSyncedFiles,
} from "./services/watch"; } from "./services/watch";
import { clearConvertToMP4Results } from "./stream";
/** /**
* Listen for IPC events sent/invoked by the renderer process, and route them to * Listen for IPC events sent/invoked by the renderer process, and route them to
@ -107,10 +102,6 @@ export const attachIPCHandlers = () => {
ipcMain.handle("selectDirectory", () => selectDirectory()); ipcMain.handle("selectDirectory", () => selectDirectory());
ipcMain.on("clearStores", () => clearStores());
ipcMain.on("clearConvertToMP4Results", () => clearConvertToMP4Results());
ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) => ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) =>
saveEncryptionKey(encryptionKey), saveEncryptionKey(encryptionKey),
); );
@ -265,6 +256,12 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
ipcMain.handle("watchFindFiles", (_, folderPath: string) => ipcMain.handle("watchFindFiles", (_, folderPath: string) =>
watchFindFiles(folderPath), watchFindFiles(folderPath),
); );
};
ipcMain.handle("watchReset", () => watchReset(watcher));
/**
* Sibling of {@link attachIPCHandlers} specifically for use with the logout
* event with needs access to the {@link FSWatcher} instance.
*/
export const attachLogoutIPCHandler = (watcher: FSWatcher) => {
ipcMain.handle("logout", () => logout(watcher));
}; };

View file

@ -0,0 +1,30 @@
import type { FSWatcher } from "chokidar";
import log from "../log";
import { clearConvertToMP4Results } from "../stream";
import { clearStores } from "./store";
import { watchReset } from "./watch";
/**
* Perform the native side logout sequence.
*
* This function is guaranteed not to throw any errors.
*
* See: [Note: Do not throw during logout].
*/
export const logout = (watcher: FSWatcher) => {
try {
watchReset(watcher);
} catch (e) {
log.error("Ignoring error during logout (FS watch)", e);
}
try {
clearConvertToMP4Results();
} catch (e) {
log.error("Ignoring error during logout (convert-to-mp4)", e);
}
try {
clearStores();
} catch (e) {
log.error("Ignoring error during logout (native stores)", e);
}
};

View file

@ -151,6 +151,15 @@ export const watchFindFiles = async (dirPath: string) => {
return paths; return paths;
}; };
/**
* Stop watching all existing folder watches and remove any callbacks.
*
* This function is meant to be called when the user logs out. It stops
* all existing folder watches and forgets about any "on*" callback
* functions that have been registered.
*
* The persisted state itself gets cleared via {@link clearStores}.
*/
export const watchReset = (watcher: FSWatcher) => { export const watchReset = (watcher: FSWatcher) => {
watcher.unwatch(folderWatches().map((watch) => watch.folderPath)); watcher.unwatch(folderWatches().map((watch) => watch.folderPath));
}; };

View file

@ -63,10 +63,10 @@ const openLogDirectory = () => ipcRenderer.invoke("openLogDirectory");
const selectDirectory = () => ipcRenderer.invoke("selectDirectory"); const selectDirectory = () => ipcRenderer.invoke("selectDirectory");
const clearStores = () => ipcRenderer.send("clearStores"); const logout = () => {
watchRemoveListeners();
const clearConvertToMP4Results = () => ipcRenderer.send("logout");
ipcRenderer.send("clearConvertToMP4Results"); };
const encryptionKey = () => ipcRenderer.invoke("encryptionKey"); const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
@ -212,11 +212,10 @@ const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => {
const watchFindFiles = (folderPath: string) => const watchFindFiles = (folderPath: string) =>
ipcRenderer.invoke("watchFindFiles", folderPath); ipcRenderer.invoke("watchFindFiles", folderPath);
const watchReset = async () => { const watchRemoveListeners = () => {
ipcRenderer.removeAllListeners("watchAddFile"); ipcRenderer.removeAllListeners("watchAddFile");
ipcRenderer.removeAllListeners("watchRemoveFile"); ipcRenderer.removeAllListeners("watchRemoveFile");
ipcRenderer.removeAllListeners("watchRemoveDir"); ipcRenderer.removeAllListeners("watchRemoveDir");
await ipcRenderer.invoke("watchReset");
}; };
// - Upload // - Upload
@ -308,8 +307,7 @@ contextBridge.exposeInMainWorld("electron", {
openDirectory, openDirectory,
openLogDirectory, openLogDirectory,
selectDirectory, selectDirectory,
clearStores, logout,
clearConvertToMP4Results,
encryptionKey, encryptionKey,
saveEncryptionKey, saveEncryptionKey,
onMainWindowFocus, onMainWindowFocus,
@ -360,7 +358,6 @@ contextBridge.exposeInMainWorld("electron", {
onRemoveFile: watchOnRemoveFile, onRemoveFile: watchOnRemoveFile,
onRemoveDir: watchOnRemoveDir, onRemoveDir: watchOnRemoveDir,
findFiles: watchFindFiles, findFiles: watchFindFiles,
reset: watchReset,
}, },
// - Upload // - Upload

View file

@ -1,6 +1,8 @@
import { CustomHead } from "@/next/components/Head"; import { CustomHead } from "@/next/components/Head";
import { setupI18n } from "@/next/i18n"; import { setupI18n } from "@/next/i18n";
import { logUnhandledErrorsAndRejections } from "@/next/log-web"; import { logUnhandledErrorsAndRejections } from "@/next/log-web";
import { PAGES } from "@ente/accounts/constants/pages";
import { accountLogout } from "@ente/accounts/services/logout";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants"; import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container"; import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
@ -27,6 +29,7 @@ interface AppContextProps {
isMobile: boolean; isMobile: boolean;
showNavBar: (show: boolean) => void; showNavBar: (show: boolean) => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2; setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
logout: () => void;
} }
export const AppContext = createContext<AppContextProps>({} as AppContextProps); export const AppContext = createContext<AppContextProps>({} as AppContextProps);
@ -78,6 +81,10 @@ export default function App({ Component, pageProps }: AppProps) {
const theme = getTheme(themeColor, APPS.PHOTOS); const theme = getTheme(themeColor, APPS.PHOTOS);
const logout = () => {
void accountLogout().then(() => router.push(PAGES.ROOT));
};
const title = isI18nReady const title = isI18nReady
? t("TITLE", { context: APPS.ACCOUNTS }) ? t("TITLE", { context: APPS.ACCOUNTS })
: APP_TITLES.get(APPS.ACCOUNTS); : APP_TITLES.get(APPS.ACCOUNTS);
@ -101,6 +108,7 @@ export default function App({ Component, pageProps }: AppProps) {
showNavBar, showNavBar,
setDialogBoxAttributesV2: setDialogBoxAttributesV2:
setDialogBoxAttributesV2 as any, setDialogBoxAttributesV2 as any,
logout,
}} }}
> >
{!isI18nReady && ( {!isI18nReady && (

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,10 @@ export default function App({ Component, pageProps }: AppProps) {
content: t("UNKNOWN_ERROR"), content: t("UNKNOWN_ERROR"),
}); });
const logout = () => {
void accountLogout().then(() => 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 +168,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,18 +1,17 @@
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
import ChevronRight from "@mui/icons-material/ChevronRight"; import ChevronRight from "@mui/icons-material/ChevronRight";
import ScienceIcon from "@mui/icons-material/Science"; import ScienceIcon from "@mui/icons-material/Science";
import { Box, DialogProps, Stack, Typography } from "@mui/material"; import { Box, DialogProps, Stack, Typography } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer"; import { EnteDrawer } from "components/EnteDrawer";
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle"; import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import Titlebar from "components/Titlebar"; import Titlebar from "components/Titlebar";
import { MLSearchSettings } from "components/ml/MLSearchSettings"; import { MLSearchSettings } from "components/ml/MLSearchSettings";
import { t } from "i18next"; import { t } from "i18next";
import { useContext, useEffect, useState } from "react";
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import isElectron from "is-electron"; import isElectron from "is-electron";
import { AppContext } from "pages/_app"; import { AppContext } from "pages/_app";
import { useContext, useEffect, useState } from "react";
import { CLIPIndexingStatus, clipService } from "services/clip-service"; import { CLIPIndexingStatus, clipService } from "services/clip-service";
import { formatNumber } from "utils/number/format"; import { formatNumber } from "utils/number/format";

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

@ -26,7 +26,6 @@ import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { MessageContainer } from "@ente/shared/components/MessageContainer"; import { MessageContainer } from "@ente/shared/components/MessageContainer";
import AppNavbar from "@ente/shared/components/Navbar/app"; import AppNavbar from "@ente/shared/components/Navbar/app";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { Events, eventBus } from "@ente/shared/events";
import { useLocalState } from "@ente/shared/hooks/useLocalState"; import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService"; import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
@ -52,7 +51,8 @@ import "photoswipe/dist/photoswipe.css";
import { createContext, useEffect, useRef, useState } from "react"; 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 { resumeExportsIfNeeded } from "services/export";
import { photosLogout } from "services/logout";
import { import {
getMLSearchConfig, getMLSearchConfig,
updateMLSearchConfig, updateMLSearchConfig,
@ -100,6 +100,7 @@ type AppContextType = {
setDialogBoxAttributesV2: SetDialogBoxAttributesV2; setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
isCFProxyDisabled: boolean; isCFProxyDisabled: boolean;
setIsCFProxyDisabled: (disabled: boolean) => void; setIsCFProxyDisabled: (disabled: boolean) => void;
logout: () => void;
}; };
export const AppContext = createContext<AppContextType>(null); export const AppContext = createContext<AppContextType>(null);
@ -188,14 +189,6 @@ export default function App({ Component, pageProps }: AppProps) {
} }
}; };
loadMlSearchState(); loadMlSearchState();
try {
eventBus.on(Events.LOGOUT, () => {
setMlSearchEnabled(false);
mlWorkManager.setMlSearchEnabled(false);
});
} catch (e) {
log.error("Error while subscribing to logout event", e);
}
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -213,13 +206,6 @@ export default function App({ Component, pageProps }: AppProps) {
await resumeExportsIfNeeded(); await resumeExportsIfNeeded();
}; };
initExport(); initExport();
try {
eventBus.on(Events.LOGOUT, () => {
exportService.disableContinuousExport();
});
} catch (e) {
log.error("Error while subscribing to logout event", e);
}
}, []); }, []);
const setUserOnline = () => setOffline(false); const setUserOnline = () => setOffline(false);
@ -336,6 +322,11 @@ export default function App({ Component, pageProps }: AppProps) {
content: t("UNKNOWN_ERROR"), content: t("UNKNOWN_ERROR"),
}); });
const logout = () => {
setMlSearchEnabled(false);
void photosLogout().then(() => 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 +385,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) {
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);
@ -672,7 +687,7 @@ export default function Gallery() {
}, [collections, hiddenCollections]); }, [collections, hiddenCollections]);
const showSessionExpiredMessage = () => { const showSessionExpiredMessage = () => {
setDialogMessage(getSessionExpiredMessage()); setDialogMessage(getSessionExpiredMessage(logout));
}; };
const syncWithRemote = async (force = false, silent = false) => { const syncWithRemote = async (force = false, silent = false) => {

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

@ -80,21 +80,20 @@ class CLIPService {
this.liveEmbeddingExtractionQueue = new PQueue({ this.liveEmbeddingExtractionQueue = new PQueue({
concurrency: 1, concurrency: 1,
}); });
eventBus.on(Events.LOGOUT, this.logoutHandler, this);
} }
isPlatformSupported = () => { isPlatformSupported = () => {
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

@ -6,7 +6,6 @@ import { APPS } from "@ente/shared/apps/constants";
import ComlinkCryptoWorker from "@ente/shared/crypto"; import ComlinkCryptoWorker from "@ente/shared/crypto";
import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker"; import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker";
import { CustomError } from "@ente/shared/error"; import { CustomError } from "@ente/shared/error";
import { Events, eventBus } from "@ente/shared/events";
import { isPlaybackPossible } from "@ente/shared/media/video-playback"; import { isPlaybackPossible } from "@ente/shared/media/video-playback";
import { Remote } from "comlink"; import { Remote } from "comlink";
import isElectron from "is-electron"; import isElectron from "is-electron";
@ -107,7 +106,6 @@ class DownloadManagerImpl {
// } // }
this.cryptoWorker = await ComlinkCryptoWorker.getInstance(); this.cryptoWorker = await ComlinkCryptoWorker.getInstance();
this.ready = true; this.ready = true;
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);
} }
private ensureInitialized() { private ensureInitialized() {
@ -117,21 +115,15 @@ class DownloadManagerImpl {
); );
} }
private async logoutHandler() { async logout() {
try { this.ready = false;
log.info("downloadManger logoutHandler started"); this.cryptoWorker = null;
this.ready = false; this.downloadClient = null;
this.cryptoWorker = null; this.fileObjectURLPromises.clear();
this.downloadClient = null; this.fileConversionPromises.clear();
this.fileObjectURLPromises.clear(); this.thumbnailObjectURLPromises.clear();
this.fileConversionPromises.clear(); this.fileDownloadProgress.clear();
this.thumbnailObjectURLPromises.clear(); this.progressUpdater = () => {};
this.fileDownloadProgress.clear();
this.progressUpdater = () => {};
log.info("downloadManager logoutHandler completed");
} catch (e) {
log.error("downloadManager logoutHandler failed", e);
}
} }
updateToken(token: string, passwordToken?: string) { updateToken(token: string, passwordToken?: string) {

View file

@ -0,0 +1,50 @@
import log from "@/next/log";
import { accountLogout } from "@ente/accounts/services/logout";
import { clipService } from "services/clip-service";
import DownloadManager from "./download";
import exportService from "./export";
import mlWorkManager from "./machineLearning/mlWorkManager";
/**
* 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();
try {
await DownloadManager.logout();
} catch (e) {
log.error("Ignoring error during logout (download)", e);
}
try {
await clipService.logout();
} catch (e) {
log.error("Ignoring error during logout (CLIP)", e);
}
const electron = globalThis.electron;
if (electron) {
try {
await mlWorkManager.logout();
} catch (e) {
log.error("Ignoring error during logout (ML)", e);
}
try {
exportService.disableContinuousExport();
} catch (e) {
log.error("Ignoring error during logout (export)", e);
}
try {
await electron?.logout();
} catch (e) {
log.error("Ignoring error during logout (electron)", e);
}
}
};

View file

@ -11,9 +11,9 @@ import {
} from "services/ml/types"; } from "services/ml/types";
import { imageBitmapToBlob, warpAffineFloat32List } from "utils/image"; import { imageBitmapToBlob, warpAffineFloat32List } from "utils/image";
import ReaderService, { import ReaderService, {
fetchImageBitmap,
getFaceId, getFaceId,
getLocalFile, getLocalFile,
getOriginalImageBitmap,
} from "./readerService"; } from "./readerService";
class FaceService { class FaceService {
@ -296,7 +296,7 @@ class FaceService {
} }
const file = await getLocalFile(personFace.fileId); const file = await getLocalFile(personFace.fileId);
const imageBitmap = await getOriginalImageBitmap(file); const imageBitmap = await fetchImageBitmap(file);
return await this.saveFaceCrop(imageBitmap, personFace, syncContext); return await this.saveFaceCrop(imageBitmap, personFace, syncContext);
} }
} }

View file

@ -14,7 +14,6 @@ import { getLocalFiles } from "services/fileService";
import mlIDbStorage, { import mlIDbStorage, {
ML_SEARCH_CONFIG_NAME, ML_SEARCH_CONFIG_NAME,
ML_SYNC_CONFIG_NAME, ML_SYNC_CONFIG_NAME,
ML_SYNC_JOB_CONFIG_NAME,
} from "services/ml/db"; } from "services/ml/db";
import { import {
BlurDetectionMethod, BlurDetectionMethod,
@ -48,19 +47,11 @@ import dbscanClusteringService from "./dbscanClusteringService";
import FaceService from "./faceService"; import FaceService from "./faceService";
import hdbscanClusteringService from "./hdbscanClusteringService"; import hdbscanClusteringService from "./hdbscanClusteringService";
import laplacianBlurDetectionService from "./laplacianBlurDetectionService"; import laplacianBlurDetectionService from "./laplacianBlurDetectionService";
import type { JobConfig } from "./mlWorkManager";
import mobileFaceNetEmbeddingService from "./mobileFaceNetEmbeddingService"; import mobileFaceNetEmbeddingService from "./mobileFaceNetEmbeddingService";
import PeopleService from "./peopleService"; import PeopleService from "./peopleService";
import ReaderService from "./readerService"; import ReaderService from "./readerService";
import yoloFaceDetectionService from "./yoloFaceDetectionService"; import yoloFaceDetectionService from "./yoloFaceDetectionService";
export const DEFAULT_ML_SYNC_JOB_CONFIG: JobConfig = {
intervalSec: 5,
// TODO: finalize this after seeing effects on and from machine sleep
maxItervalSec: 960,
backoffMultiplier: 2,
};
export const DEFAULT_ML_SYNC_CONFIG: MLSyncConfig = { export const DEFAULT_ML_SYNC_CONFIG: MLSyncConfig = {
batchSize: 200, batchSize: 200,
imageSource: "Original", imageSource: "Original",
@ -108,13 +99,6 @@ export const DEFAULT_ML_SEARCH_CONFIG: MLSearchConfig = {
export const MAX_ML_SYNC_ERROR_COUNT = 1; export const MAX_ML_SYNC_ERROR_COUNT = 1;
export async function getMLSyncJobConfig() {
return mlIDbStorage.getConfig(
ML_SYNC_JOB_CONFIG_NAME,
DEFAULT_ML_SYNC_JOB_CONFIG,
);
}
export async function getMLSyncConfig() { export async function getMLSyncConfig() {
return mlIDbStorage.getConfig(ML_SYNC_CONFIG_NAME, DEFAULT_ML_SYNC_CONFIG); return mlIDbStorage.getConfig(ML_SYNC_CONFIG_NAME, DEFAULT_ML_SYNC_CONFIG);
} }
@ -131,14 +115,6 @@ export async function getMLSearchConfig() {
return DEFAULT_ML_SEARCH_CONFIG; return DEFAULT_ML_SEARCH_CONFIG;
} }
export async function updateMLSyncJobConfig(newConfig: JobConfig) {
return mlIDbStorage.putConfig(ML_SYNC_JOB_CONFIG_NAME, newConfig);
}
export async function updateMLSyncConfig(newConfig: MLSyncConfig) {
return mlIDbStorage.putConfig(ML_SYNC_CONFIG_NAME, newConfig);
}
export async function updateMLSearchConfig(newConfig: MLSearchConfig) { export async function updateMLSearchConfig(newConfig: MLSearchConfig) {
return mlIDbStorage.putConfig(ML_SEARCH_CONFIG_NAME, newConfig); return mlIDbStorage.putConfig(ML_SEARCH_CONFIG_NAME, newConfig);
} }

View file

@ -5,12 +5,11 @@ import { eventBus, Events } from "@ente/shared/events";
import { getToken, getUserID } from "@ente/shared/storage/localStorage/helpers"; import { getToken, getUserID } from "@ente/shared/storage/localStorage/helpers";
import debounce from "debounce"; import debounce from "debounce";
import PQueue from "p-queue"; import PQueue from "p-queue";
import { getMLSyncJobConfig } from "services/machineLearning/machineLearningService";
import mlIDbStorage from "services/ml/db"; import mlIDbStorage from "services/ml/db";
import { createFaceComlinkWorker } from "services/ml/face";
import type { DedicatedMLWorker } from "services/ml/face.worker";
import { MLSyncResult } from "services/ml/types"; import { MLSyncResult } from "services/ml/types";
import { EnteFile } from "types/file"; import { EnteFile } from "types/file";
import { getDedicatedMLWorker } from "utils/comlink/ComlinkMLWorker";
import { DedicatedMLWorker } from "worker/ml.worker";
import { logQueueStats } from "./machineLearningService"; import { logQueueStats } from "./machineLearningService";
const LIVE_SYNC_IDLE_DEBOUNCE_SEC = 30; const LIVE_SYNC_IDLE_DEBOUNCE_SEC = 30;
@ -21,32 +20,30 @@ export type JobState = "Scheduled" | "Running" | "NotScheduled";
export interface JobConfig { export interface JobConfig {
intervalSec: number; intervalSec: number;
maxItervalSec: number;
backoffMultiplier: number; backoffMultiplier: number;
} }
export interface JobResult { export interface MLSyncJobResult {
shouldBackoff: boolean; shouldBackoff: boolean;
mlSyncResult: MLSyncResult;
} }
export class SimpleJob<R extends JobResult> { export class MLSyncJob {
private config: JobConfig; private runCallback: () => Promise<MLSyncJobResult>;
private runCallback: () => Promise<R>;
private state: JobState; private state: JobState;
private stopped: boolean; private stopped: boolean;
private intervalSec: number; private intervalSec: number;
private nextTimeoutId: ReturnType<typeof setTimeout>; private nextTimeoutId: ReturnType<typeof setTimeout>;
constructor(config: JobConfig, runCallback: () => Promise<R>) { constructor(runCallback: () => Promise<MLSyncJobResult>) {
this.config = config;
this.runCallback = runCallback; this.runCallback = runCallback;
this.state = "NotScheduled"; this.state = "NotScheduled";
this.stopped = true; this.stopped = true;
this.intervalSec = this.config.intervalSec; this.resetInterval();
} }
public resetInterval() { public resetInterval() {
this.intervalSec = this.config.intervalSec; this.intervalSec = 5;
} }
public start() { public start() {
@ -79,10 +76,7 @@ export class SimpleJob<R extends JobResult> {
try { try {
const jobResult = await this.runCallback(); const jobResult = await this.runCallback();
if (jobResult && jobResult.shouldBackoff) { if (jobResult && jobResult.shouldBackoff) {
this.intervalSec = Math.min( this.intervalSec = Math.min(960, this.intervalSec * 2);
this.config.maxItervalSec,
this.intervalSec * this.config.backoffMultiplier,
);
} else { } else {
this.resetInterval(); this.resetInterval();
} }
@ -109,12 +103,6 @@ export class SimpleJob<R extends JobResult> {
} }
} }
export interface MLSyncJobResult extends JobResult {
mlSyncResult: MLSyncResult;
}
export class MLSyncJob extends SimpleJob<MLSyncJobResult> {}
class MLWorkManager { class MLWorkManager {
private mlSyncJob: MLSyncJob; private mlSyncJob: MLSyncJob;
private syncJobWorker: ComlinkWorker<typeof DedicatedMLWorker>; private syncJobWorker: ComlinkWorker<typeof DedicatedMLWorker>;
@ -135,7 +123,6 @@ class MLWorkManager {
}); });
this.mlSearchEnabled = false; this.mlSearchEnabled = false;
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);
this.debouncedLiveSyncIdle = debounce( this.debouncedLiveSyncIdle = debounce(
() => this.onLiveSyncIdle(), () => this.onLiveSyncIdle(),
LIVE_SYNC_IDLE_DEBOUNCE_SEC * 1000, LIVE_SYNC_IDLE_DEBOUNCE_SEC * 1000,
@ -187,26 +174,12 @@ class MLWorkManager {
} }
} }
// Handlers async logout() {
private async appStartHandler() { this.setMlSearchEnabled(false);
log.info("appStartHandler"); this.stopSyncJob();
try { this.mlSyncJob = undefined;
this.startSyncJob(); await this.terminateLiveSyncWorker();
} catch (e) { await mlIDbStorage.clearMLDB();
log.error("Failed in ML appStart Handler", e);
}
}
private async logoutHandler() {
log.info("logoutHandler");
try {
this.stopSyncJob();
this.mlSyncJob = undefined;
await this.terminateLiveSyncWorker();
await mlIDbStorage.clearMLDB();
} catch (e) {
log.error("Failed in ML logout Handler", e);
}
} }
private async fileUploadedHandler(arg: { private async fileUploadedHandler(arg: {
@ -238,7 +211,7 @@ class MLWorkManager {
// Live Sync // Live Sync
private async getLiveSyncWorker() { private async getLiveSyncWorker() {
if (!this.liveSyncWorker) { if (!this.liveSyncWorker) {
this.liveSyncWorker = getDedicatedMLWorker("ml-sync-live"); this.liveSyncWorker = createFaceComlinkWorker("ml-sync-live");
} }
return await this.liveSyncWorker.remote; return await this.liveSyncWorker.remote;
@ -286,7 +259,7 @@ class MLWorkManager {
// Sync Job // Sync Job
private async getSyncJobWorker() { private async getSyncJobWorker() {
if (!this.syncJobWorker) { if (!this.syncJobWorker) {
this.syncJobWorker = getDedicatedMLWorker("ml-sync-job"); this.syncJobWorker = createFaceComlinkWorker("ml-sync-job");
} }
return await this.syncJobWorker.remote; return await this.syncJobWorker.remote;
@ -344,11 +317,8 @@ class MLWorkManager {
log.info("User not logged in, not starting ml sync job"); log.info("User not logged in, not starting ml sync job");
return; return;
} }
const mlSyncJobConfig = await getMLSyncJobConfig();
if (!this.mlSyncJob) { if (!this.mlSyncJob) {
this.mlSyncJob = new MLSyncJob(mlSyncJobConfig, () => this.mlSyncJob = new MLSyncJob(() => this.runMLSyncJob());
this.runMLSyncJob(),
);
} }
this.mlSyncJob.start(); this.mlSyncJob.start();
} catch (e) { } catch (e) {

View file

@ -2,7 +2,7 @@ import log from "@/next/log";
import mlIDbStorage from "services/ml/db"; import mlIDbStorage from "services/ml/db";
import { Face, MLSyncContext, Person } from "services/ml/types"; import { Face, MLSyncContext, Person } from "services/ml/types";
import FaceService, { isDifferentOrOld } from "./faceService"; import FaceService, { isDifferentOrOld } from "./faceService";
import { getLocalFile, getOriginalImageBitmap } from "./readerService"; import { fetchImageBitmap, getLocalFile } from "./readerService";
class PeopleService { class PeopleService {
async syncPeopleIndex(syncContext: MLSyncContext) { async syncPeopleIndex(syncContext: MLSyncContext) {
@ -58,7 +58,7 @@ class PeopleService {
if (personFace && !personFace.crop?.cacheKey) { if (personFace && !personFace.crop?.cacheKey) {
const file = await getLocalFile(personFace.fileId); const file = await getLocalFile(personFace.fileId);
const imageBitmap = await getOriginalImageBitmap(file); const imageBitmap = await fetchImageBitmap(file);
await FaceService.saveFaceCrop( await FaceService.saveFaceCrop(
imageBitmap, imageBitmap,
personFace, personFace,

View file

@ -1,7 +1,6 @@
import { FILE_TYPE } from "@/media/file-type"; import { FILE_TYPE } from "@/media/file-type";
import { decodeLivePhoto } from "@/media/live-photo"; import { decodeLivePhoto } from "@/media/live-photo";
import log from "@/next/log"; import log from "@/next/log";
import PQueue from "p-queue";
import DownloadManager from "services/download"; import DownloadManager from "services/download";
import { getLocalFiles } from "services/fileService"; import { getLocalFiles } from "services/fileService";
import { Dimensions } from "services/ml/geom"; import { Dimensions } from "services/ml/geom";
@ -41,7 +40,7 @@ class ReaderService {
fileContext.enteFile.metadata.fileType, fileContext.enteFile.metadata.fileType,
) )
) { ) {
fileContext.imageBitmap = await getOriginalImageBitmap( fileContext.imageBitmap = await fetchImageBitmap(
fileContext.enteFile, fileContext.enteFile,
); );
} else { } else {
@ -106,22 +105,12 @@ export function getFaceId(detectedFace: DetectedFace, imageDims: Dimensions) {
return faceID; return faceID;
} }
async function getImageBlobBitmap(blob: Blob): Promise<ImageBitmap> { export const fetchImageBitmap = async (file: EnteFile) =>
return await createImageBitmap(blob); fetchRenderableBlob(file).then(createImageBitmap);
}
async function getOriginalFile(file: EnteFile, queue?: PQueue) { async function fetchRenderableBlob(file: EnteFile) {
let fileStream; const fileStream = await DownloadManager.getFile(file);
if (queue) { const fileBlob = await new Response(fileStream).blob();
fileStream = await queue.add(() => DownloadManager.getFile(file));
} else {
fileStream = await DownloadManager.getFile(file);
}
return new Response(fileStream).blob();
}
async function getOriginalConvertedFile(file: EnteFile, queue?: PQueue) {
const fileBlob = await getOriginalFile(file, queue);
if (file.metadata.fileType === FILE_TYPE.IMAGE) { if (file.metadata.fileType === FILE_TYPE.IMAGE) {
return await getRenderableImage(file.metadata.title, fileBlob); return await getRenderableImage(file.metadata.title, fileBlob);
} else { } else {
@ -133,17 +122,11 @@ async function getOriginalConvertedFile(file: EnteFile, queue?: PQueue) {
} }
} }
export async function getOriginalImageBitmap(file: EnteFile, queue?: PQueue) {
const fileBlob = await getOriginalConvertedFile(file, queue);
log.info("[MLService] Got file: ", file.id.toString());
return getImageBlobBitmap(fileBlob);
}
export async function getThumbnailImageBitmap(file: EnteFile) { export async function getThumbnailImageBitmap(file: EnteFile) {
const thumb = await DownloadManager.getThumbnail(file); const thumb = await DownloadManager.getThumbnail(file);
log.info("[MLService] Got thumbnail: ", file.id.toString()); log.info("[MLService] Got thumbnail: ", file.id.toString());
return getImageBlobBitmap(new Blob([thumb])); return createImageBitmap(new Blob([thumb]));
} }
export async function getLocalFileImageBitmap( export async function getLocalFileImageBitmap(
@ -152,5 +135,5 @@ export async function getLocalFileImageBitmap(
) { ) {
let fileBlob = localFile as Blob; let fileBlob = localFile as Blob;
fileBlob = await getRenderableImage(enteFile.metadata.title, fileBlob); fileBlob = await getRenderableImage(enteFile.metadata.title, fileBlob);
return getImageBlobBitmap(fileBlob); return createImageBitmap(fileBlob);
} }

View file

@ -12,7 +12,6 @@ import isElectron from "is-electron";
import { import {
DEFAULT_ML_SEARCH_CONFIG, DEFAULT_ML_SEARCH_CONFIG,
DEFAULT_ML_SYNC_CONFIG, DEFAULT_ML_SYNC_CONFIG,
DEFAULT_ML_SYNC_JOB_CONFIG,
MAX_ML_SYNC_ERROR_COUNT, MAX_ML_SYNC_ERROR_COUNT,
} from "services/machineLearning/machineLearningService"; } from "services/machineLearning/machineLearningService";
import { Face, MLLibraryData, MlFileData, Person } from "services/ml/types"; import { Face, MLLibraryData, MlFileData, Person } from "services/ml/types";
@ -27,7 +26,6 @@ export interface IndexStatus {
interface Config {} interface Config {}
export const ML_SYNC_JOB_CONFIG_NAME = "ml-sync-job";
export const ML_SYNC_CONFIG_NAME = "ml-sync"; export const ML_SYNC_CONFIG_NAME = "ml-sync";
export const ML_SEARCH_CONFIG_NAME = "ml-search"; export const ML_SEARCH_CONFIG_NAME = "ml-search";
@ -136,12 +134,14 @@ class MLIDbStorage {
// TODO: update configs if version is updated in defaults // TODO: update configs if version is updated in defaults
db.createObjectStore("configs"); db.createObjectStore("configs");
/*
await tx await tx
.objectStore("configs") .objectStore("configs")
.add( .add(
DEFAULT_ML_SYNC_JOB_CONFIG, DEFAULT_ML_SYNC_JOB_CONFIG,
ML_SYNC_JOB_CONFIG_NAME, "ml-sync-job",
); );
*/
await tx await tx
.objectStore("configs") .objectStore("configs")
.add(DEFAULT_ML_SYNC_CONFIG, ML_SYNC_CONFIG_NAME); .add(DEFAULT_ML_SYNC_CONFIG, ML_SYNC_CONFIG_NAME);
@ -163,6 +163,10 @@ class MLIDbStorage {
.objectStore("configs") .objectStore("configs")
.delete(ML_SEARCH_CONFIG_NAME); .delete(ML_SEARCH_CONFIG_NAME);
await tx
.objectStore("configs")
.delete("ml-sync-job");
await tx await tx
.objectStore("configs") .objectStore("configs")
.add( .add(

View file

@ -0,0 +1,8 @@
import { ComlinkWorker } from "@/next/worker/comlink-worker";
import type { DedicatedMLWorker } from "services/ml/face.worker";
const createFaceWebWorker = () =>
new Worker(new URL("face.worker.ts", import.meta.url));
export const createFaceComlinkWorker = (name: string) =>
new ComlinkWorker<typeof DedicatedMLWorker>(name, createFaceWebWorker());

View file

@ -1,11 +1,8 @@
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/user";
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
import { ApiError } from "@ente/shared/error"; import { ApiError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService"; import HTTPService from "@ente/shared/network/HTTPService";
import { getEndpoint, getFamilyPortalURL } from "@ente/shared/network/api"; import { getEndpoint, getFamilyPortalURL } from "@ente/shared/network/api";
import localForage from "@ente/shared/storage/localForage";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { import {
getToken, getToken,
@ -104,10 +101,6 @@ export const getRoadmapRedirectURL = async () => {
} }
}; };
export const clearFiles = async () => {
await localForage.clear();
};
export const isTokenValid = async (token: string) => { export const isTokenValid = async (token: string) => {
try { try {
const resp = await HTTPService.get( const resp = await HTTPService.get(
@ -233,19 +226,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

@ -1,13 +0,0 @@
import { haveWindow } from "@/next/env";
import { ComlinkWorker } from "@/next/worker/comlink-worker";
import { type DedicatedMLWorker } from "worker/ml.worker";
export const getDedicatedMLWorker = (name: string) => {
if (haveWindow()) {
const cryptoComlinkWorker = new ComlinkWorker<typeof DedicatedMLWorker>(
name ?? "ente-ml-worker",
new Worker(new URL("worker/ml.worker.ts", import.meta.url)),
);
return cryptoComlinkWorker;
}
};

View file

@ -1,6 +1,5 @@
import { ensureElectron } from "@/next/electron"; import { ensureElectron } from "@/next/electron";
import { AppUpdate } from "@/next/types/ipc"; import { AppUpdate } from "@/next/types/ipc";
import { logoutUser } from "@ente/accounts/services/user";
import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types"; import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined"; import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined";
import InfoOutlined from "@mui/icons-material/InfoRounded"; import InfoOutlined from "@mui/icons-material/InfoRounded";
@ -121,14 +120,16 @@ export const getSubscriptionPurchaseSuccessMessage = (
), ),
}); });
export const getSessionExpiredMessage = (): DialogBoxAttributes => ({ export const getSessionExpiredMessage = (
action: () => void,
): DialogBoxAttributes => ({
title: t("SESSION_EXPIRED"), title: t("SESSION_EXPIRED"),
content: t("SESSION_EXPIRED_MESSAGE"), content: t("SESSION_EXPIRED_MESSAGE"),
nonClosable: true, nonClosable: true,
proceed: { proceed: {
text: t("LOGIN"), text: t("LOGIN"),
action: logoutUser, action,
variant: "accent", variant: "accent",
}, },
}); });

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

@ -50,10 +50,11 @@ import {
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) {
const { logout } = appContext;
const [srpAttributes, setSrpAttributes] = useState<SRPAttributes>(); const [srpAttributes, setSrpAttributes] = useState<SRPAttributes>();
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>(); const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
@ -275,7 +276,7 @@ export default function Credentials({ appContext, appName }: PageProps) {
<LinkButton onClick={redirectToRecoverPage}> <LinkButton onClick={redirectToRecoverPage}>
{t("FORGOT_PASSWORD")} {t("FORGOT_PASSWORD")}
</LinkButton> </LinkButton>
<LinkButton onClick={logoutUser}> <LinkButton onClick={logout}>
{t("CHANGE_EMAIL")} {t("CHANGE_EMAIL")}
</LinkButton> </LinkButton>
</FormPaperFooter> </FormPaperFooter>

View file

@ -1,7 +1,6 @@
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 { 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,
@ -31,6 +30,8 @@ import { KeyAttributes, User } from "@ente/shared/user/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
export default function Generate({ appContext, appName }: PageProps) { export default function Generate({ appContext, appName }: PageProps) {
const { logout } = appContext;
const [token, setToken] = useState<string>(); const [token, setToken] = useState<string>();
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
const [recoverModalView, setRecoveryModalView] = useState(false); const [recoverModalView, setRecoveryModalView] = useState(false);
@ -113,7 +114,7 @@ export default function Generate({ appContext, appName }: PageProps) {
buttonText={t("SET_PASSPHRASE")} buttonText={t("SET_PASSPHRASE")}
/> />
<FormPaperFooter> <FormPaperFooter>
<LinkButton onClick={logoutUser}> <LinkButton onClick={logout}>
{t("GO_BACK")} {t("GO_BACK")}
</LinkButton> </LinkButton>
</FormPaperFooter> </FormPaperFooter>

View file

@ -2,7 +2,6 @@ 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 { 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";
@ -33,6 +32,8 @@ export default function Recover({
appContext, appContext,
twoFactorType = TwoFactorType.TOTP, twoFactorType = TwoFactorType.TOTP,
}: PageProps) { }: PageProps) {
const { logout } = appContext;
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] = const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
useState<B64EncryptionResult>(null); useState<B64EncryptionResult>(null);
const [sessionID, setSessionID] = useState(null); const [sessionID, setSessionID] = useState(null);
@ -77,7 +78,7 @@ export default function Recover({
e instanceof ApiError && e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.NotFound e.httpStatusCode === HttpStatusCode.NotFound
) { ) {
logoutUser(); logout();
} else { } else {
log.error("two factor recovery page setup failed", e); log.error("two factor recovery page setup failed", e);
setDoesHaveEncryptedRecoveryKey(false); setDoesHaveEncryptedRecoveryKey(false);

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 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";
@ -19,7 +19,11 @@ import { t } from "i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const TwoFactorVerify: React.FC<PageProps> = () => { export const TwoFactorVerify: React.FC<PageProps> = ({
appContext,
}: PageProps) => {
const { logout } = appContext;
const [sessionID, setSessionID] = useState(""); const [sessionID, setSessionID] = useState("");
const router = useRouter(); const router = useRouter();
@ -60,7 +64,7 @@ export const TwoFactorVerify: React.FC<PageProps> = () => {
e instanceof ApiError && e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.NotFound e.httpStatusCode === HttpStatusCode.NotFound
) { ) {
logoutUser(); logout();
} else { } else {
throw e; throw e;
} }
@ -79,7 +83,7 @@ export const TwoFactorVerify: React.FC<PageProps> = () => {
> >
{t("LOST_DEVICE")} {t("LOST_DEVICE")}
</LinkButton> </LinkButton>
<LinkButton onClick={logoutUser}> <LinkButton onClick={logout}>
{t("CHANGE_EMAIL")} {t("CHANGE_EMAIL")}
</LinkButton> </LinkButton>
</FormPaperFooter> </FormPaperFooter>

View file

@ -16,7 +16,7 @@ import SingleInputForm, {
import { ApiError } from "@ente/shared/error"; import { ApiError } from "@ente/shared/error";
import { getAccountsURL } from "@ente/shared/network/api"; import { getAccountsURL } from "@ente/shared/network/api";
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore"; import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
import { clearFiles } from "@ente/shared/storage/localForage/helpers"; import localForage from "@ente/shared/storage/localForage";
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage"; import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
import { import {
getLocalReferralSource, getLocalReferralSource,
@ -30,10 +30,11 @@ 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 { 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) {
const { logout } = appContext;
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [resend, setResend] = useState(0); const [resend, setResend] = useState(0);
@ -121,7 +122,7 @@ export default function VerifyPage({ appContext, appName }: PageProps) {
await configureSRP(srpSetupAttributes); await configureSRP(srpSetupAttributes);
} }
} }
clearFiles(); localForage.clear();
setIsFirstLogin(true); setIsFirstLogin(true);
const redirectURL = InMemoryStore.get(MS_KEYS.REDIRECT_URL); const redirectURL = InMemoryStore.get(MS_KEYS.REDIRECT_URL);
InMemoryStore.delete(MS_KEYS.REDIRECT_URL); InMemoryStore.delete(MS_KEYS.REDIRECT_URL);
@ -191,7 +192,7 @@ export default function VerifyPage({ appContext, appName }: PageProps) {
)} )}
{resend === 1 && <span>{t("SENDING")}</span>} {resend === 1 && <span>{t("SENDING")}</span>}
{resend === 2 && <span>{t("SENT")}</span>} {resend === 2 && <span>{t("SENT")}</span>}
<LinkButton onClick={logoutUser}> <LinkButton onClick={logout}>
{t("CHANGE_EMAIL")} {t("CHANGE_EMAIL")}
</LinkButton> </LinkButton>
</FormPaperFooter> </FormPaperFooter>

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 localForage 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 logout (remote)", e);
}
try {
InMemoryStore.clear();
} catch (e) {
log.error("Ignoring error during logout (in-memory store)", e);
}
try {
clearKeys();
} catch (e) {
log.error("Ignoring error during logout (session store)", e);
}
try {
clearData();
} catch (e) {
log.error("Ignoring error during logout (local storage)", e);
}
try {
await localForage.clear();
} catch (e) {
log.error("Ignoring error during logout (local forage)", e);
}
try {
await clearCaches();
} catch (e) {
log.error("Ignoring error during logout (cache)", 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

@ -64,19 +64,9 @@ export interface Electron {
selectDirectory: () => Promise<string | undefined>; selectDirectory: () => Promise<string | undefined>;
/** /**
* Clear any stored data. * Perform any logout related cleanup of native side state.
*
* This is a coarse single shot cleanup, meant for use in clearing any
* persisted Electron side state during logout.
*/ */
clearStores: () => void; logout: () => Promise<void>;
/**
* Clear an state corresponding to in-flight convert-to-mp4 requests.
*
* This is meant for use during logout.
*/
clearConvertToMP4Results: () => void;
/** /**
* Return the previously saved encryption key from persistent safe storage. * Return the previously saved encryption key from persistent safe storage.
@ -487,17 +477,6 @@ export interface Electron {
* The returned paths are guaranteed to use POSIX separators ('/'). * The returned paths are guaranteed to use POSIX separators ('/').
*/ */
findFiles: (folderPath: string) => Promise<string[]>; findFiles: (folderPath: string) => Promise<string[]>;
/**
* Stop watching all existing folder watches and remove any callbacks.
*
* This function is meant to be called when the user logs out. It stops
* all existing folder watches and forgets about any "on*" callback
* functions that have been registered.
*
* The persisted state itself gets cleared via {@link clearStores}.
*/
reset: () => Promise<void>;
}; };
// - Upload // - Upload

View file

@ -7,6 +7,7 @@ export interface PageProps {
showNavBar: (show: boolean) => void; showNavBar: (show: boolean) => void;
isMobile: boolean; isMobile: boolean;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2; setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
logout: () => void;
}; };
appName: APPS; appName: APPS;
twoFactorType?: TwoFactorType; twoFactorType?: TwoFactorType;

View file

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