commit
69460418ef
40 changed files with 313 additions and 339 deletions
|
@ -17,7 +17,11 @@ import { existsSync } from "node:fs";
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
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 { createApplicationMenu, createTrayContextMenu } from "./main/menu";
|
||||
import { setupAutoUpdater } from "./main/services/app-update";
|
||||
|
@ -377,8 +381,12 @@ const main = () => {
|
|||
void (async () => {
|
||||
// Create window and prepare for the renderer.
|
||||
mainWindow = createMainWindow();
|
||||
|
||||
// Setup IPC and streams.
|
||||
const watcher = createWatcher(mainWindow);
|
||||
attachIPCHandlers();
|
||||
attachFSWatchIPCHandlers(createWatcher(mainWindow));
|
||||
attachFSWatchIPCHandlers(watcher);
|
||||
attachLogoutIPCHandler(watcher);
|
||||
registerStreamProtocol();
|
||||
|
||||
// Configure the renderer's environment.
|
||||
|
|
|
@ -41,16 +41,13 @@ import {
|
|||
fsWriteFile,
|
||||
} from "./services/fs";
|
||||
import { convertToJPEG, generateImageThumbnail } from "./services/image";
|
||||
import { logout } from "./services/logout";
|
||||
import {
|
||||
clipImageEmbedding,
|
||||
clipTextEmbeddingIfAvailable,
|
||||
} from "./services/ml-clip";
|
||||
import { detectFaces, faceEmbedding } from "./services/ml-face";
|
||||
import {
|
||||
clearStores,
|
||||
encryptionKey,
|
||||
saveEncryptionKey,
|
||||
} from "./services/store";
|
||||
import { encryptionKey, saveEncryptionKey } from "./services/store";
|
||||
import {
|
||||
clearPendingUploads,
|
||||
listZipItems,
|
||||
|
@ -65,11 +62,9 @@ import {
|
|||
watchFindFiles,
|
||||
watchGet,
|
||||
watchRemove,
|
||||
watchReset,
|
||||
watchUpdateIgnoredFiles,
|
||||
watchUpdateSyncedFiles,
|
||||
} from "./services/watch";
|
||||
import { clearConvertToMP4Results } from "./stream";
|
||||
|
||||
/**
|
||||
* 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.on("clearStores", () => clearStores());
|
||||
|
||||
ipcMain.on("clearConvertToMP4Results", () => clearConvertToMP4Results());
|
||||
|
||||
ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) =>
|
||||
saveEncryptionKey(encryptionKey),
|
||||
);
|
||||
|
@ -265,6 +256,12 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
|
|||
ipcMain.handle("watchFindFiles", (_, folderPath: string) =>
|
||||
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));
|
||||
};
|
||||
|
|
30
desktop/src/main/services/logout.ts
Normal file
30
desktop/src/main/services/logout.ts
Normal 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);
|
||||
}
|
||||
};
|
|
@ -151,6 +151,15 @@ export const watchFindFiles = async (dirPath: string) => {
|
|||
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) => {
|
||||
watcher.unwatch(folderWatches().map((watch) => watch.folderPath));
|
||||
};
|
||||
|
|
|
@ -63,10 +63,10 @@ const openLogDirectory = () => ipcRenderer.invoke("openLogDirectory");
|
|||
|
||||
const selectDirectory = () => ipcRenderer.invoke("selectDirectory");
|
||||
|
||||
const clearStores = () => ipcRenderer.send("clearStores");
|
||||
|
||||
const clearConvertToMP4Results = () =>
|
||||
ipcRenderer.send("clearConvertToMP4Results");
|
||||
const logout = () => {
|
||||
watchRemoveListeners();
|
||||
ipcRenderer.send("logout");
|
||||
};
|
||||
|
||||
const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
|
||||
|
||||
|
@ -212,11 +212,10 @@ const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => {
|
|||
const watchFindFiles = (folderPath: string) =>
|
||||
ipcRenderer.invoke("watchFindFiles", folderPath);
|
||||
|
||||
const watchReset = async () => {
|
||||
const watchRemoveListeners = () => {
|
||||
ipcRenderer.removeAllListeners("watchAddFile");
|
||||
ipcRenderer.removeAllListeners("watchRemoveFile");
|
||||
ipcRenderer.removeAllListeners("watchRemoveDir");
|
||||
await ipcRenderer.invoke("watchReset");
|
||||
};
|
||||
|
||||
// - Upload
|
||||
|
@ -308,8 +307,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
openDirectory,
|
||||
openLogDirectory,
|
||||
selectDirectory,
|
||||
clearStores,
|
||||
clearConvertToMP4Results,
|
||||
logout,
|
||||
encryptionKey,
|
||||
saveEncryptionKey,
|
||||
onMainWindowFocus,
|
||||
|
@ -360,7 +358,6 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
onRemoveFile: watchOnRemoveFile,
|
||||
onRemoveDir: watchOnRemoveDir,
|
||||
findFiles: watchFindFiles,
|
||||
reset: watchReset,
|
||||
},
|
||||
|
||||
// - Upload
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { CustomHead } from "@/next/components/Head";
|
||||
import { setupI18n } from "@/next/i18n";
|
||||
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 { Overlay } from "@ente/shared/components/Container";
|
||||
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
|
||||
|
@ -27,6 +29,7 @@ interface AppContextProps {
|
|||
isMobile: boolean;
|
||||
showNavBar: (show: boolean) => void;
|
||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
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 logout = () => {
|
||||
void accountLogout().then(() => router.push(PAGES.ROOT));
|
||||
};
|
||||
|
||||
const title = isI18nReady
|
||||
? t("TITLE", { context: APPS.ACCOUNTS })
|
||||
: APP_TITLES.get(APPS.ACCOUNTS);
|
||||
|
@ -101,6 +108,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
showNavBar,
|
||||
setDialogBoxAttributesV2:
|
||||
setDialogBoxAttributesV2 as any,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
{!isI18nReady && (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,10 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
content: t("UNKNOWN_ERROR"),
|
||||
});
|
||||
|
||||
const logout = () => {
|
||||
void accountLogout().then(() => router.push(PAGES.ROOT));
|
||||
};
|
||||
|
||||
const title = isI18nReady
|
||||
? t("TITLE", { context: APPS.AUTH })
|
||||
: APP_TITLES.get(APPS.AUTH);
|
||||
|
@ -162,6 +168,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
setThemeColor,
|
||||
somethingWentWrong,
|
||||
setDialogBoxAttributesV2,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
{(loading || !isI18nReady) && (
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
|
||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||
import ScienceIcon from "@mui/icons-material/Science";
|
||||
import { Box, DialogProps, Stack, Typography } from "@mui/material";
|
||||
import { EnteDrawer } from "components/EnteDrawer";
|
||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
||||
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
|
||||
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
|
||||
import Titlebar from "components/Titlebar";
|
||||
import { MLSearchSettings } from "components/ml/MLSearchSettings";
|
||||
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 { AppContext } from "pages/_app";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { CLIPIndexingStatus, clipService } from "services/clip-service";
|
||||
import { formatNumber } from "utils/number/format";
|
||||
|
||||
|
|
|
@ -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") },
|
||||
|
|
|
@ -26,7 +26,6 @@ import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
|||
import { MessageContainer } from "@ente/shared/components/MessageContainer";
|
||||
import AppNavbar from "@ente/shared/components/Navbar/app";
|
||||
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 HTTPService from "@ente/shared/network/HTTPService";
|
||||
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 LoadingBar from "react-top-loading-bar";
|
||||
import DownloadManager from "services/download";
|
||||
import exportService, { resumeExportsIfNeeded } from "services/export";
|
||||
import { resumeExportsIfNeeded } from "services/export";
|
||||
import { photosLogout } from "services/logout";
|
||||
import {
|
||||
getMLSearchConfig,
|
||||
updateMLSearchConfig,
|
||||
|
@ -100,6 +100,7 @@ type AppContextType = {
|
|||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
||||
isCFProxyDisabled: boolean;
|
||||
setIsCFProxyDisabled: (disabled: boolean) => void;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
export const AppContext = createContext<AppContextType>(null);
|
||||
|
@ -188,14 +189,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
}
|
||||
};
|
||||
loadMlSearchState();
|
||||
try {
|
||||
eventBus.on(Events.LOGOUT, () => {
|
||||
setMlSearchEnabled(false);
|
||||
mlWorkManager.setMlSearchEnabled(false);
|
||||
});
|
||||
} catch (e) {
|
||||
log.error("Error while subscribing to logout event", e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -213,13 +206,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
await resumeExportsIfNeeded();
|
||||
};
|
||||
initExport();
|
||||
try {
|
||||
eventBus.on(Events.LOGOUT, () => {
|
||||
exportService.disableContinuousExport();
|
||||
});
|
||||
} catch (e) {
|
||||
log.error("Error while subscribing to logout event", e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setUserOnline = () => setOffline(false);
|
||||
|
@ -336,6 +322,11 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
content: t("UNKNOWN_ERROR"),
|
||||
});
|
||||
|
||||
const logout = () => {
|
||||
setMlSearchEnabled(false);
|
||||
void photosLogout().then(() => router.push(PAGES.ROOT));
|
||||
};
|
||||
|
||||
const title = isI18nReady
|
||||
? t("TITLE", { context: APPS.PHOTOS })
|
||||
: APP_TITLES.get(APPS.PHOTOS);
|
||||
|
@ -394,6 +385,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
updateMapEnabled,
|
||||
isCFProxyDisabled,
|
||||
setIsCFProxyDisabled,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
{(loading || !isI18nReady) && (
|
||||
|
|
|
@ -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) {
|
||||
logout();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
appContext.showNavBar(true);
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
|
@ -672,7 +687,7 @@ export default function Gallery() {
|
|||
}, [collections, hiddenCollections]);
|
||||
|
||||
const showSessionExpiredMessage = () => {
|
||||
setDialogMessage(getSessionExpiredMessage());
|
||||
setDialogMessage(getSessionExpiredMessage(logout));
|
||||
};
|
||||
|
||||
const syncWithRemote = async (force = false, silent = false) => {
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -80,21 +80,20 @@ class CLIPService {
|
|||
this.liveEmbeddingExtractionQueue = new PQueue({
|
||||
concurrency: 1,
|
||||
});
|
||||
eventBus.on(Events.LOGOUT, this.logoutHandler, this);
|
||||
}
|
||||
|
||||
isPlatformSupported = () => {
|
||||
return isElectron();
|
||||
};
|
||||
|
||||
private logoutHandler = async () => {
|
||||
async logout() {
|
||||
if (this.embeddingExtractionInProgress) {
|
||||
this.embeddingExtractionInProgress.abort();
|
||||
}
|
||||
if (this.onFileUploadedHandler) {
|
||||
await this.removeOnFileUploadListener();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setupOnFileUploadListener = async () => {
|
||||
try {
|
||||
|
|
|
@ -6,7 +6,6 @@ import { APPS } from "@ente/shared/apps/constants";
|
|||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { Events, eventBus } from "@ente/shared/events";
|
||||
import { isPlaybackPossible } from "@ente/shared/media/video-playback";
|
||||
import { Remote } from "comlink";
|
||||
import isElectron from "is-electron";
|
||||
|
@ -107,7 +106,6 @@ class DownloadManagerImpl {
|
|||
// }
|
||||
this.cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
this.ready = true;
|
||||
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);
|
||||
}
|
||||
|
||||
private ensureInitialized() {
|
||||
|
@ -117,21 +115,15 @@ class DownloadManagerImpl {
|
|||
);
|
||||
}
|
||||
|
||||
private async logoutHandler() {
|
||||
try {
|
||||
log.info("downloadManger logoutHandler started");
|
||||
this.ready = false;
|
||||
this.cryptoWorker = null;
|
||||
this.downloadClient = null;
|
||||
this.fileObjectURLPromises.clear();
|
||||
this.fileConversionPromises.clear();
|
||||
this.thumbnailObjectURLPromises.clear();
|
||||
this.fileDownloadProgress.clear();
|
||||
this.progressUpdater = () => {};
|
||||
log.info("downloadManager logoutHandler completed");
|
||||
} catch (e) {
|
||||
log.error("downloadManager logoutHandler failed", e);
|
||||
}
|
||||
async logout() {
|
||||
this.ready = false;
|
||||
this.cryptoWorker = null;
|
||||
this.downloadClient = null;
|
||||
this.fileObjectURLPromises.clear();
|
||||
this.fileConversionPromises.clear();
|
||||
this.thumbnailObjectURLPromises.clear();
|
||||
this.fileDownloadProgress.clear();
|
||||
this.progressUpdater = () => {};
|
||||
}
|
||||
|
||||
updateToken(token: string, passwordToken?: string) {
|
||||
|
|
50
web/apps/photos/src/services/logout.ts
Normal file
50
web/apps/photos/src/services/logout.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -11,9 +11,9 @@ import {
|
|||
} from "services/ml/types";
|
||||
import { imageBitmapToBlob, warpAffineFloat32List } from "utils/image";
|
||||
import ReaderService, {
|
||||
fetchImageBitmap,
|
||||
getFaceId,
|
||||
getLocalFile,
|
||||
getOriginalImageBitmap,
|
||||
} from "./readerService";
|
||||
|
||||
class FaceService {
|
||||
|
@ -296,7 +296,7 @@ class FaceService {
|
|||
}
|
||||
|
||||
const file = await getLocalFile(personFace.fileId);
|
||||
const imageBitmap = await getOriginalImageBitmap(file);
|
||||
const imageBitmap = await fetchImageBitmap(file);
|
||||
return await this.saveFaceCrop(imageBitmap, personFace, syncContext);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import { getLocalFiles } from "services/fileService";
|
|||
import mlIDbStorage, {
|
||||
ML_SEARCH_CONFIG_NAME,
|
||||
ML_SYNC_CONFIG_NAME,
|
||||
ML_SYNC_JOB_CONFIG_NAME,
|
||||
} from "services/ml/db";
|
||||
import {
|
||||
BlurDetectionMethod,
|
||||
|
@ -48,19 +47,11 @@ import dbscanClusteringService from "./dbscanClusteringService";
|
|||
import FaceService from "./faceService";
|
||||
import hdbscanClusteringService from "./hdbscanClusteringService";
|
||||
import laplacianBlurDetectionService from "./laplacianBlurDetectionService";
|
||||
import type { JobConfig } from "./mlWorkManager";
|
||||
import mobileFaceNetEmbeddingService from "./mobileFaceNetEmbeddingService";
|
||||
import PeopleService from "./peopleService";
|
||||
import ReaderService from "./readerService";
|
||||
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 = {
|
||||
batchSize: 200,
|
||||
imageSource: "Original",
|
||||
|
@ -108,13 +99,6 @@ export const DEFAULT_ML_SEARCH_CONFIG: MLSearchConfig = {
|
|||
|
||||
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() {
|
||||
return mlIDbStorage.getConfig(ML_SYNC_CONFIG_NAME, DEFAULT_ML_SYNC_CONFIG);
|
||||
}
|
||||
|
@ -131,14 +115,6 @@ export async function getMLSearchConfig() {
|
|||
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) {
|
||||
return mlIDbStorage.putConfig(ML_SEARCH_CONFIG_NAME, newConfig);
|
||||
}
|
||||
|
|
|
@ -5,12 +5,11 @@ import { eventBus, Events } from "@ente/shared/events";
|
|||
import { getToken, getUserID } from "@ente/shared/storage/localStorage/helpers";
|
||||
import debounce from "debounce";
|
||||
import PQueue from "p-queue";
|
||||
import { getMLSyncJobConfig } from "services/machineLearning/machineLearningService";
|
||||
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 { EnteFile } from "types/file";
|
||||
import { getDedicatedMLWorker } from "utils/comlink/ComlinkMLWorker";
|
||||
import { DedicatedMLWorker } from "worker/ml.worker";
|
||||
import { logQueueStats } from "./machineLearningService";
|
||||
|
||||
const LIVE_SYNC_IDLE_DEBOUNCE_SEC = 30;
|
||||
|
@ -21,32 +20,30 @@ export type JobState = "Scheduled" | "Running" | "NotScheduled";
|
|||
|
||||
export interface JobConfig {
|
||||
intervalSec: number;
|
||||
maxItervalSec: number;
|
||||
backoffMultiplier: number;
|
||||
}
|
||||
|
||||
export interface JobResult {
|
||||
export interface MLSyncJobResult {
|
||||
shouldBackoff: boolean;
|
||||
mlSyncResult: MLSyncResult;
|
||||
}
|
||||
|
||||
export class SimpleJob<R extends JobResult> {
|
||||
private config: JobConfig;
|
||||
private runCallback: () => Promise<R>;
|
||||
export class MLSyncJob {
|
||||
private runCallback: () => Promise<MLSyncJobResult>;
|
||||
private state: JobState;
|
||||
private stopped: boolean;
|
||||
private intervalSec: number;
|
||||
private nextTimeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(config: JobConfig, runCallback: () => Promise<R>) {
|
||||
this.config = config;
|
||||
constructor(runCallback: () => Promise<MLSyncJobResult>) {
|
||||
this.runCallback = runCallback;
|
||||
this.state = "NotScheduled";
|
||||
this.stopped = true;
|
||||
this.intervalSec = this.config.intervalSec;
|
||||
this.resetInterval();
|
||||
}
|
||||
|
||||
public resetInterval() {
|
||||
this.intervalSec = this.config.intervalSec;
|
||||
this.intervalSec = 5;
|
||||
}
|
||||
|
||||
public start() {
|
||||
|
@ -79,10 +76,7 @@ export class SimpleJob<R extends JobResult> {
|
|||
try {
|
||||
const jobResult = await this.runCallback();
|
||||
if (jobResult && jobResult.shouldBackoff) {
|
||||
this.intervalSec = Math.min(
|
||||
this.config.maxItervalSec,
|
||||
this.intervalSec * this.config.backoffMultiplier,
|
||||
);
|
||||
this.intervalSec = Math.min(960, this.intervalSec * 2);
|
||||
} else {
|
||||
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 {
|
||||
private mlSyncJob: MLSyncJob;
|
||||
private syncJobWorker: ComlinkWorker<typeof DedicatedMLWorker>;
|
||||
|
@ -135,7 +123,6 @@ class MLWorkManager {
|
|||
});
|
||||
this.mlSearchEnabled = false;
|
||||
|
||||
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);
|
||||
this.debouncedLiveSyncIdle = debounce(
|
||||
() => this.onLiveSyncIdle(),
|
||||
LIVE_SYNC_IDLE_DEBOUNCE_SEC * 1000,
|
||||
|
@ -187,26 +174,12 @@ class MLWorkManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Handlers
|
||||
private async appStartHandler() {
|
||||
log.info("appStartHandler");
|
||||
try {
|
||||
this.startSyncJob();
|
||||
} catch (e) {
|
||||
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);
|
||||
}
|
||||
async logout() {
|
||||
this.setMlSearchEnabled(false);
|
||||
this.stopSyncJob();
|
||||
this.mlSyncJob = undefined;
|
||||
await this.terminateLiveSyncWorker();
|
||||
await mlIDbStorage.clearMLDB();
|
||||
}
|
||||
|
||||
private async fileUploadedHandler(arg: {
|
||||
|
@ -238,7 +211,7 @@ class MLWorkManager {
|
|||
// Live Sync
|
||||
private async getLiveSyncWorker() {
|
||||
if (!this.liveSyncWorker) {
|
||||
this.liveSyncWorker = getDedicatedMLWorker("ml-sync-live");
|
||||
this.liveSyncWorker = createFaceComlinkWorker("ml-sync-live");
|
||||
}
|
||||
|
||||
return await this.liveSyncWorker.remote;
|
||||
|
@ -286,7 +259,7 @@ class MLWorkManager {
|
|||
// Sync Job
|
||||
private async getSyncJobWorker() {
|
||||
if (!this.syncJobWorker) {
|
||||
this.syncJobWorker = getDedicatedMLWorker("ml-sync-job");
|
||||
this.syncJobWorker = createFaceComlinkWorker("ml-sync-job");
|
||||
}
|
||||
|
||||
return await this.syncJobWorker.remote;
|
||||
|
@ -344,11 +317,8 @@ class MLWorkManager {
|
|||
log.info("User not logged in, not starting ml sync job");
|
||||
return;
|
||||
}
|
||||
const mlSyncJobConfig = await getMLSyncJobConfig();
|
||||
if (!this.mlSyncJob) {
|
||||
this.mlSyncJob = new MLSyncJob(mlSyncJobConfig, () =>
|
||||
this.runMLSyncJob(),
|
||||
);
|
||||
this.mlSyncJob = new MLSyncJob(() => this.runMLSyncJob());
|
||||
}
|
||||
this.mlSyncJob.start();
|
||||
} catch (e) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import log from "@/next/log";
|
|||
import mlIDbStorage from "services/ml/db";
|
||||
import { Face, MLSyncContext, Person } from "services/ml/types";
|
||||
import FaceService, { isDifferentOrOld } from "./faceService";
|
||||
import { getLocalFile, getOriginalImageBitmap } from "./readerService";
|
||||
import { fetchImageBitmap, getLocalFile } from "./readerService";
|
||||
|
||||
class PeopleService {
|
||||
async syncPeopleIndex(syncContext: MLSyncContext) {
|
||||
|
@ -58,7 +58,7 @@ class PeopleService {
|
|||
|
||||
if (personFace && !personFace.crop?.cacheKey) {
|
||||
const file = await getLocalFile(personFace.fileId);
|
||||
const imageBitmap = await getOriginalImageBitmap(file);
|
||||
const imageBitmap = await fetchImageBitmap(file);
|
||||
await FaceService.saveFaceCrop(
|
||||
imageBitmap,
|
||||
personFace,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { FILE_TYPE } from "@/media/file-type";
|
||||
import { decodeLivePhoto } from "@/media/live-photo";
|
||||
import log from "@/next/log";
|
||||
import PQueue from "p-queue";
|
||||
import DownloadManager from "services/download";
|
||||
import { getLocalFiles } from "services/fileService";
|
||||
import { Dimensions } from "services/ml/geom";
|
||||
|
@ -41,7 +40,7 @@ class ReaderService {
|
|||
fileContext.enteFile.metadata.fileType,
|
||||
)
|
||||
) {
|
||||
fileContext.imageBitmap = await getOriginalImageBitmap(
|
||||
fileContext.imageBitmap = await fetchImageBitmap(
|
||||
fileContext.enteFile,
|
||||
);
|
||||
} else {
|
||||
|
@ -106,22 +105,12 @@ export function getFaceId(detectedFace: DetectedFace, imageDims: Dimensions) {
|
|||
return faceID;
|
||||
}
|
||||
|
||||
async function getImageBlobBitmap(blob: Blob): Promise<ImageBitmap> {
|
||||
return await createImageBitmap(blob);
|
||||
}
|
||||
export const fetchImageBitmap = async (file: EnteFile) =>
|
||||
fetchRenderableBlob(file).then(createImageBitmap);
|
||||
|
||||
async function getOriginalFile(file: EnteFile, queue?: PQueue) {
|
||||
let fileStream;
|
||||
if (queue) {
|
||||
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);
|
||||
async function fetchRenderableBlob(file: EnteFile) {
|
||||
const fileStream = await DownloadManager.getFile(file);
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
if (file.metadata.fileType === FILE_TYPE.IMAGE) {
|
||||
return await getRenderableImage(file.metadata.title, fileBlob);
|
||||
} 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) {
|
||||
const thumb = await DownloadManager.getThumbnail(file);
|
||||
log.info("[MLService] Got thumbnail: ", file.id.toString());
|
||||
|
||||
return getImageBlobBitmap(new Blob([thumb]));
|
||||
return createImageBitmap(new Blob([thumb]));
|
||||
}
|
||||
|
||||
export async function getLocalFileImageBitmap(
|
||||
|
@ -152,5 +135,5 @@ export async function getLocalFileImageBitmap(
|
|||
) {
|
||||
let fileBlob = localFile as Blob;
|
||||
fileBlob = await getRenderableImage(enteFile.metadata.title, fileBlob);
|
||||
return getImageBlobBitmap(fileBlob);
|
||||
return createImageBitmap(fileBlob);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import isElectron from "is-electron";
|
|||
import {
|
||||
DEFAULT_ML_SEARCH_CONFIG,
|
||||
DEFAULT_ML_SYNC_CONFIG,
|
||||
DEFAULT_ML_SYNC_JOB_CONFIG,
|
||||
MAX_ML_SYNC_ERROR_COUNT,
|
||||
} from "services/machineLearning/machineLearningService";
|
||||
import { Face, MLLibraryData, MlFileData, Person } from "services/ml/types";
|
||||
|
@ -27,7 +26,6 @@ export interface IndexStatus {
|
|||
|
||||
interface Config {}
|
||||
|
||||
export const ML_SYNC_JOB_CONFIG_NAME = "ml-sync-job";
|
||||
export const ML_SYNC_CONFIG_NAME = "ml-sync";
|
||||
export const ML_SEARCH_CONFIG_NAME = "ml-search";
|
||||
|
||||
|
@ -136,12 +134,14 @@ class MLIDbStorage {
|
|||
// TODO: update configs if version is updated in defaults
|
||||
db.createObjectStore("configs");
|
||||
|
||||
/*
|
||||
await tx
|
||||
.objectStore("configs")
|
||||
.add(
|
||||
DEFAULT_ML_SYNC_JOB_CONFIG,
|
||||
ML_SYNC_JOB_CONFIG_NAME,
|
||||
"ml-sync-job",
|
||||
);
|
||||
*/
|
||||
await tx
|
||||
.objectStore("configs")
|
||||
.add(DEFAULT_ML_SYNC_CONFIG, ML_SYNC_CONFIG_NAME);
|
||||
|
@ -163,6 +163,10 @@ class MLIDbStorage {
|
|||
.objectStore("configs")
|
||||
.delete(ML_SEARCH_CONFIG_NAME);
|
||||
|
||||
await tx
|
||||
.objectStore("configs")
|
||||
.delete("ml-sync-job");
|
||||
|
||||
await tx
|
||||
.objectStore("configs")
|
||||
.add(
|
||||
|
|
8
web/apps/photos/src/services/ml/face.ts
Normal file
8
web/apps/photos/src/services/ml/face.ts
Normal 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());
|
|
@ -1,11 +1,8 @@
|
|||
import log from "@/next/log";
|
||||
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 HTTPService from "@ente/shared/network/HTTPService";
|
||||
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 {
|
||||
getToken,
|
||||
|
@ -104,10 +101,6 @@ export const getRoadmapRedirectURL = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
export const clearFiles = async () => {
|
||||
await localForage.clear();
|
||||
};
|
||||
|
||||
export const isTokenValid = async (token: string) => {
|
||||
try {
|
||||
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 () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
import { ensureElectron } from "@/next/electron";
|
||||
import { AppUpdate } from "@/next/types/ipc";
|
||||
import { logoutUser } from "@ente/accounts/services/user";
|
||||
import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
|
||||
import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined";
|
||||
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"),
|
||||
content: t("SESSION_EXPIRED_MESSAGE"),
|
||||
|
||||
nonClosable: true,
|
||||
proceed: {
|
||||
text: t("LOGIN"),
|
||||
action: logoutUser,
|
||||
action,
|
||||
variant: "accent",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -50,10 +50,11 @@ import {
|
|||
generateSRPSetupAttributes,
|
||||
loginViaSRP,
|
||||
} from "../services/srp";
|
||||
import { logoutUser } from "../services/user";
|
||||
import { SRPAttributes } from "../types/srp";
|
||||
|
||||
export default function Credentials({ appContext, appName }: PageProps) {
|
||||
const { logout } = appContext;
|
||||
|
||||
const [srpAttributes, setSrpAttributes] = useState<SRPAttributes>();
|
||||
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
|
||||
const [user, setUser] = useState<User>();
|
||||
|
@ -275,7 +276,7 @@ export default function Credentials({ appContext, appName }: PageProps) {
|
|||
<LinkButton onClick={redirectToRecoverPage}>
|
||||
{t("FORGOT_PASSWORD")}
|
||||
</LinkButton>
|
||||
<LinkButton onClick={logoutUser}>
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import log from "@/next/log";
|
||||
import { putAttributes } from "@ente/accounts/api/user";
|
||||
import { configureSRP } from "@ente/accounts/services/srp";
|
||||
import { logoutUser } from "@ente/accounts/services/user";
|
||||
import { generateKeyAndSRPAttributes } from "@ente/accounts/utils/srp";
|
||||
import {
|
||||
generateAndSaveIntermediateKeyAttributes,
|
||||
|
@ -31,6 +30,8 @@ import { KeyAttributes, User } from "@ente/shared/user/types";
|
|||
import { useRouter } from "next/router";
|
||||
|
||||
export default function Generate({ appContext, appName }: PageProps) {
|
||||
const { logout } = appContext;
|
||||
|
||||
const [token, setToken] = useState<string>();
|
||||
const [user, setUser] = useState<User>();
|
||||
const [recoverModalView, setRecoveryModalView] = useState(false);
|
||||
|
@ -113,7 +114,7 @@ export default function Generate({ appContext, appName }: PageProps) {
|
|||
buttonText={t("SET_PASSPHRASE")}
|
||||
/>
|
||||
<FormPaperFooter>
|
||||
<LinkButton onClick={logoutUser}>
|
||||
<LinkButton onClick={logout}>
|
||||
{t("GO_BACK")}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
|
|
|
@ -2,7 +2,6 @@ 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 { PageProps } from "@ente/shared/apps/types";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
|
||||
|
@ -33,6 +32,8 @@ export default function Recover({
|
|||
appContext,
|
||||
twoFactorType = TwoFactorType.TOTP,
|
||||
}: PageProps) {
|
||||
const { logout } = appContext;
|
||||
|
||||
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
|
||||
useState<B64EncryptionResult>(null);
|
||||
const [sessionID, setSessionID] = useState(null);
|
||||
|
@ -77,7 +78,7 @@ export default function Recover({
|
|||
e instanceof ApiError &&
|
||||
e.httpStatusCode === HttpStatusCode.NotFound
|
||||
) {
|
||||
logoutUser();
|
||||
logout();
|
||||
} else {
|
||||
log.error("two factor recovery page setup failed", e);
|
||||
setDoesHaveEncryptedRecoveryKey(false);
|
||||
|
|
|
@ -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 type { PageProps } from "@ente/shared/apps/types";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
|
@ -19,7 +19,11 @@ import { t } from "i18next";
|
|||
import { useRouter } from "next/router";
|
||||
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 router = useRouter();
|
||||
|
@ -60,7 +64,7 @@ export const TwoFactorVerify: React.FC<PageProps> = () => {
|
|||
e instanceof ApiError &&
|
||||
e.httpStatusCode === HttpStatusCode.NotFound
|
||||
) {
|
||||
logoutUser();
|
||||
logout();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
@ -79,7 +83,7 @@ export const TwoFactorVerify: React.FC<PageProps> = () => {
|
|||
>
|
||||
{t("LOST_DEVICE")}
|
||||
</LinkButton>
|
||||
<LinkButton onClick={logoutUser}>
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
|
|
|
@ -16,7 +16,7 @@ import SingleInputForm, {
|
|||
import { ApiError } from "@ente/shared/error";
|
||||
import { getAccountsURL } from "@ente/shared/network/api";
|
||||
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 {
|
||||
getLocalReferralSource,
|
||||
|
@ -30,10 +30,11 @@ import { useRouter } from "next/router";
|
|||
import { putAttributes, sendOtt, verifyOtt } from "../api/user";
|
||||
import { PAGES } from "../constants/pages";
|
||||
import { configureSRP } from "../services/srp";
|
||||
import { logoutUser } from "../services/user";
|
||||
import { SRPSetupAttributes } from "../types/srp";
|
||||
|
||||
export default function VerifyPage({ appContext, appName }: PageProps) {
|
||||
const { logout } = appContext;
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [resend, setResend] = useState(0);
|
||||
|
||||
|
@ -121,7 +122,7 @@ export default function VerifyPage({ appContext, appName }: PageProps) {
|
|||
await configureSRP(srpSetupAttributes);
|
||||
}
|
||||
}
|
||||
clearFiles();
|
||||
localForage.clear();
|
||||
setIsFirstLogin(true);
|
||||
const redirectURL = InMemoryStore.get(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 === 2 && <span>{t("SENT")}</span>}
|
||||
<LinkButton onClick={logoutUser}>
|
||||
<LinkButton onClick={logout}>
|
||||
{t("CHANGE_EMAIL")}
|
||||
</LinkButton>
|
||||
</FormPaperFooter>
|
||||
|
|
50
web/packages/accounts/services/logout.ts
Normal file
50
web/packages/accounts/services/logout.ts
Normal 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);
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -64,19 +64,9 @@ export interface Electron {
|
|||
selectDirectory: () => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Clear any stored data.
|
||||
*
|
||||
* This is a coarse single shot cleanup, meant for use in clearing any
|
||||
* persisted Electron side state during logout.
|
||||
* Perform any logout related cleanup of native side state.
|
||||
*/
|
||||
clearStores: () => void;
|
||||
|
||||
/**
|
||||
* Clear an state corresponding to in-flight convert-to-mp4 requests.
|
||||
*
|
||||
* This is meant for use during logout.
|
||||
*/
|
||||
clearConvertToMP4Results: () => void;
|
||||
logout: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* 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 ('/').
|
||||
*/
|
||||
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
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface PageProps {
|
|||
showNavBar: (show: boolean) => void;
|
||||
isMobile: boolean;
|
||||
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
|
||||
logout: () => void;
|
||||
};
|
||||
appName: APPS;
|
||||
twoFactorType?: TwoFactorType;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import localForage from ".";
|
||||
|
||||
export const clearFiles = async () => {
|
||||
await localForage.clear();
|
||||
};
|
Loading…
Add table
Reference in a new issue