[web] General code improvements (#5926)
This commit is contained in:
@@ -54,7 +54,6 @@ import {
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import {
|
||||
getLocaleInUse,
|
||||
pt,
|
||||
setLocaleInUse,
|
||||
supportedLocales,
|
||||
ut,
|
||||
@@ -827,9 +826,7 @@ const Preferences: React.FC<NestedSidebarDrawerVisibilityProps> = ({
|
||||
</RowButtonGroupTitle>
|
||||
<RowButtonGroup>
|
||||
<RowSwitch
|
||||
label={
|
||||
/* TODO(HLS): */ pt("Streamable videos")
|
||||
}
|
||||
label={t("streamable_videos")}
|
||||
checked={isHLSGenerationEnabled}
|
||||
onClick={() => void toggleHLSGeneration()}
|
||||
/>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Upload, type UploadTypeSelectorIntent } from "components/Upload";
|
||||
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
|
||||
import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog";
|
||||
import { stashRedirect } from "ente-accounts/services/redirect";
|
||||
import { isSessionInvalid } from "ente-accounts/services/session";
|
||||
import type { MiniDialogAttributes } from "ente-base/components/MiniDialog";
|
||||
import { NavbarBase } from "ente-base/components/Navbar";
|
||||
import { CenteredRow } from "ente-base/components/containers";
|
||||
@@ -32,6 +33,11 @@ import { useIsSmallWidth } from "ente-base/components/utils/hooks";
|
||||
import { useModalVisibility } from "ente-base/components/utils/modal";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import log from "ente-base/log";
|
||||
import {
|
||||
clearSessionStorage,
|
||||
haveCredentialsInSession,
|
||||
masterKeyFromSessionIfLoggedIn,
|
||||
} from "ente-base/session";
|
||||
import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone";
|
||||
import { type Collection } from "ente-media/collection";
|
||||
import { type EnteFile } from "ente-media/file";
|
||||
@@ -56,6 +62,7 @@ import {
|
||||
import {
|
||||
constructUserIDToEmailMap,
|
||||
createShareeSuggestionEmails,
|
||||
validateKey,
|
||||
} from "ente-new/photos/components/gallery/helpers";
|
||||
import {
|
||||
useGalleryReducer,
|
||||
@@ -93,8 +100,6 @@ import {
|
||||
} from "ente-new/photos/services/user-details";
|
||||
import { usePhotosAppContext } from "ente-new/photos/types/context";
|
||||
import { FlexWrapper } from "ente-shared/components/Container";
|
||||
import { getRecoveryKey } from "ente-shared/crypto/helpers";
|
||||
import { CustomError } from "ente-shared/error";
|
||||
import { getData } from "ente-shared/storage/localStorage";
|
||||
import {
|
||||
getToken,
|
||||
@@ -103,7 +108,7 @@ import {
|
||||
setIsFirstLogin,
|
||||
setJustSignedUp,
|
||||
} from "ente-shared/storage/localStorage/helpers";
|
||||
import { clearKeys, getKey } from "ente-shared/storage/sessionStorage";
|
||||
import { getKey } from "ente-shared/storage/sessionStorage";
|
||||
import { t } from "i18next";
|
||||
import { useRouter, type NextRouter } from "next/router";
|
||||
import { createContext, useCallback, useEffect, useRef, useState } from "react";
|
||||
@@ -117,7 +122,6 @@ import {
|
||||
} from "services/collectionService";
|
||||
import exportService from "services/export";
|
||||
import { uploadManager } from "services/upload-manager";
|
||||
import { isTokenValid } from "services/userService";
|
||||
import {
|
||||
GalleryContextType,
|
||||
SelectedState,
|
||||
@@ -131,6 +135,17 @@ import {
|
||||
} from "utils/collection";
|
||||
import { getSelectedFiles, handleFileOp, type FileOp } from "utils/file";
|
||||
|
||||
/**
|
||||
* Options to customize the behaviour of the sync with remote that gets
|
||||
* triggered on various actions within the gallery and its descendants.
|
||||
*/
|
||||
interface SyncWithRemoteOpts {
|
||||
/** Force a sync to happen (default: no) */
|
||||
force?: boolean;
|
||||
/** Perform the sync without showing a global loading bar (default: no) */
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
const defaultGalleryContext: GalleryContextType = {
|
||||
setActiveCollectionID: () => null,
|
||||
syncWithRemote: () => null,
|
||||
@@ -187,13 +202,11 @@ const Page: React.FC = () => {
|
||||
);
|
||||
const [isFileViewerOpen, setIsFileViewerOpen] = useState(false);
|
||||
|
||||
const syncInProgress = useRef(false);
|
||||
const syncInterval = useRef<ReturnType<typeof setInterval> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const resync = useRef<{ force: boolean; silent: boolean } | undefined>(
|
||||
undefined,
|
||||
);
|
||||
/**`true` if a sync is currently in progress. */
|
||||
const isSyncing = useRef(false);
|
||||
/** Set to the {@link SyncWithRemoteOpts} of the last sync that was enqueued
|
||||
while one was already in progress. */
|
||||
const resyncOpts = useRef<SyncWithRemoteOpts | undefined>(undefined);
|
||||
|
||||
const [userIDToEmailMap, setUserIDToEmailMap] =
|
||||
useState<Map<number, string>>(null);
|
||||
@@ -286,32 +299,21 @@ const Page: React.FC = () => {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 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 {
|
||||
logout();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const key = getKey("encryptionKey");
|
||||
const token = getToken();
|
||||
if (!key || !token) {
|
||||
if (!haveCredentialsInSession() || !token) {
|
||||
stashRedirect("/gallery");
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
preloadImage("/images/subscription-card-background");
|
||||
|
||||
const electron = globalThis.electron;
|
||||
const main = async () => {
|
||||
const valid = await validateKey();
|
||||
if (!valid) {
|
||||
let syncIntervalID: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
void (async () => {
|
||||
if (!(await validateKey())) {
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
initSettings();
|
||||
@@ -335,21 +337,23 @@ const Page: React.FC = () => {
|
||||
hiddenFiles: await getLocalFiles("hidden"),
|
||||
trashedFiles: await getLocalTrashedFiles(),
|
||||
});
|
||||
await syncWithRemote(true);
|
||||
await syncWithRemote({ force: true });
|
||||
setIsFirstLoad(false);
|
||||
setJustSignedUp(false);
|
||||
syncInterval.current = setInterval(
|
||||
() => syncWithRemote(false, true),
|
||||
syncIntervalID = setInterval(
|
||||
() => syncWithRemote({ silent: true }),
|
||||
5 * 60 * 1000 /* 5 minutes */,
|
||||
);
|
||||
if (electron) {
|
||||
electron.onMainWindowFocus(() => syncWithRemote(false, true));
|
||||
electron.onMainWindowFocus(() =>
|
||||
syncWithRemote({ silent: true }),
|
||||
);
|
||||
if (await shouldShowWhatsNew(electron)) showWhatsNew();
|
||||
}
|
||||
};
|
||||
main();
|
||||
})();
|
||||
|
||||
return () => {
|
||||
clearInterval(syncInterval.current);
|
||||
clearInterval(syncIntervalID);
|
||||
if (electron) electron.onMainWindowFocus(undefined);
|
||||
};
|
||||
}, []);
|
||||
@@ -522,7 +526,7 @@ const Page: React.FC = () => {
|
||||
setTimeout(hideLoadingBar, 0);
|
||||
}, [showLoadingBar, hideLoadingBar]);
|
||||
|
||||
const handleFileAndCollectionSyncWithRemote = useCallback(async () => {
|
||||
const fileAndCollectionSyncWithRemote = useCallback(async () => {
|
||||
const didUpdateFiles = await syncCollectionAndFiles({
|
||||
onSetCollections: (
|
||||
collections,
|
||||
@@ -551,27 +555,39 @@ const Page: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSyncWithRemote = useCallback(
|
||||
async (force = false, silent = false) => {
|
||||
const syncWithRemote = useCallback(
|
||||
async (opts?: SyncWithRemoteOpts) => {
|
||||
const { force, silent } = opts ?? {};
|
||||
|
||||
// Pre-flight checks.
|
||||
if (!navigator.onLine) return;
|
||||
if (syncInProgress.current && !force) {
|
||||
resync.current = { force, silent };
|
||||
if (await isSessionInvalid()) {
|
||||
showSessionExpiredDialog();
|
||||
return;
|
||||
}
|
||||
const isForced = syncInProgress.current && force;
|
||||
syncInProgress.current = true;
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
if (!(await masterKeyFromSessionIfLoggedIn())) {
|
||||
clearSessionStorage();
|
||||
router.push("/credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start or enqueue.
|
||||
let isForced = false;
|
||||
if (isSyncing.current) {
|
||||
if (force) {
|
||||
isForced = true;
|
||||
} else {
|
||||
resyncOpts.current = { force, silent };
|
||||
return;
|
||||
}
|
||||
const tokenValid = await isTokenValid(token);
|
||||
if (!tokenValid) {
|
||||
throw new Error(CustomError.SESSION_EXPIRED);
|
||||
}
|
||||
!silent && showLoadingBar();
|
||||
}
|
||||
|
||||
// The sync
|
||||
isSyncing.current = true;
|
||||
try {
|
||||
if (!silent) showLoadingBar();
|
||||
await preCollectionAndFilesSync();
|
||||
await handleFileAndCollectionSyncWithRemote();
|
||||
await fileAndCollectionSyncWithRemote();
|
||||
// syncWithRemote is called with the force flag set to true before
|
||||
// doing an upload. So it is possible, say when resuming a pending
|
||||
// upload, that we get two syncWithRemotes happening in parallel.
|
||||
@@ -581,26 +597,17 @@ const Page: React.FC = () => {
|
||||
await postCollectionAndFilesSync();
|
||||
}
|
||||
} catch (e) {
|
||||
switch (e.message) {
|
||||
case CustomError.SESSION_EXPIRED:
|
||||
showSessionExpiredDialog();
|
||||
break;
|
||||
case CustomError.KEY_MISSING:
|
||||
clearKeys();
|
||||
router.push("/credentials");
|
||||
break;
|
||||
default:
|
||||
log.error("syncWithRemote failed", e);
|
||||
}
|
||||
log.error("syncWithRemote failed", e);
|
||||
} finally {
|
||||
dispatch({ type: "clearUnsyncedState" });
|
||||
!silent && hideLoadingBar();
|
||||
if (!silent) hideLoadingBar();
|
||||
}
|
||||
syncInProgress.current = false;
|
||||
if (resync.current) {
|
||||
const { force, silent } = resync.current;
|
||||
setTimeout(() => handleSyncWithRemote(force, silent), 0);
|
||||
resync.current = undefined;
|
||||
isSyncing.current = false;
|
||||
|
||||
const nextOpts = resyncOpts.current;
|
||||
if (nextOpts) {
|
||||
resyncOpts.current = undefined;
|
||||
setTimeout(() => syncWithRemote(nextOpts), 0);
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -608,13 +615,10 @@ const Page: React.FC = () => {
|
||||
hideLoadingBar,
|
||||
router,
|
||||
showSessionExpiredDialog,
|
||||
handleFileAndCollectionSyncWithRemote,
|
||||
fileAndCollectionSyncWithRemote,
|
||||
],
|
||||
);
|
||||
|
||||
// Alias for existing code.
|
||||
const syncWithRemote = handleSyncWithRemote;
|
||||
|
||||
const setupSelectAllKeyBoardShortcutHandler = () => {
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
@@ -688,7 +692,7 @@ const Page: React.FC = () => {
|
||||
);
|
||||
}
|
||||
clearSelection();
|
||||
await syncWithRemote(false, true);
|
||||
await syncWithRemote({ silent: true });
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
@@ -724,7 +728,7 @@ const Page: React.FC = () => {
|
||||
);
|
||||
}
|
||||
clearSelection();
|
||||
await syncWithRemote(false, true);
|
||||
await syncWithRemote({ silent: true });
|
||||
} catch (e) {
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
@@ -897,7 +901,8 @@ const Page: React.FC = () => {
|
||||
value={{
|
||||
...defaultGalleryContext,
|
||||
setActiveCollectionID: handleSetActiveCollectionID,
|
||||
syncWithRemote,
|
||||
syncWithRemote: (force, silent) =>
|
||||
syncWithRemote({ force, silent }),
|
||||
setBlockingLoad,
|
||||
photoListHeader,
|
||||
userIDToEmailMap,
|
||||
@@ -1047,7 +1052,9 @@ const Page: React.FC = () => {
|
||||
|
||||
<Upload
|
||||
activeCollection={activeCollection}
|
||||
syncWithRemote={syncWithRemote}
|
||||
syncWithRemote={(force, silent) =>
|
||||
syncWithRemote({ force, silent })
|
||||
}
|
||||
closeUploadTypeSelector={setUploadTypeSelectorView.bind(
|
||||
null,
|
||||
false,
|
||||
@@ -1131,7 +1138,7 @@ const Page: React.FC = () => {
|
||||
}
|
||||
onMarkTempDeleted={handleMarkTempDeleted}
|
||||
onSetOpenFileViewer={setIsFileViewerOpen}
|
||||
onSyncWithRemote={handleSyncWithRemote}
|
||||
onSyncWithRemote={syncWithRemote}
|
||||
onVisualFeedback={handleVisualFeedback}
|
||||
onSelectCollection={handleSelectCollection}
|
||||
onSelectPerson={handleSelectPerson}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { HttpStatusCode } from "axios";
|
||||
import { putAttributes } from "ente-accounts/services/user";
|
||||
import log from "ente-base/log";
|
||||
import { apiURL } from "ente-base/origins";
|
||||
import type { UserDetails } from "ente-new/photos/services/user-details";
|
||||
import { ApiError } from "ente-shared/error";
|
||||
import HTTPService from "ente-shared/network/HTTPService";
|
||||
import { getData } from "ente-shared/storage/localStorage";
|
||||
import { getToken } from "ente-shared/storage/localStorage/helpers";
|
||||
|
||||
const HAS_SET_KEYS = "hasSetKeys";
|
||||
|
||||
export const getPublicKey = async (email: string) => {
|
||||
const token = getToken();
|
||||
|
||||
@@ -21,44 +15,6 @@ export const getPublicKey = async (email: string) => {
|
||||
return resp.data.publicKey;
|
||||
};
|
||||
|
||||
export const isTokenValid = async (token: string) => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
await apiURL("/users/session-validity/v2"),
|
||||
null,
|
||||
{ "X-Auth-Token": token },
|
||||
);
|
||||
try {
|
||||
if (resp.data[HAS_SET_KEYS] === undefined) {
|
||||
throw Error("resp.data.hasSetKey undefined");
|
||||
}
|
||||
if (!resp.data.hasSetKeys) {
|
||||
try {
|
||||
await putAttributes(
|
||||
token,
|
||||
getData("originalKeyAttributes"),
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("put attribute failed", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("hasSetKeys not set in session validity response", e);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
log.error("session-validity api call failed", e);
|
||||
if (
|
||||
e instanceof ApiError &&
|
||||
e.httpStatusCode === HttpStatusCode.Unauthorized
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserDetailsV2 = async (): Promise<UserDetails> => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
configureSRP,
|
||||
generateKeyAndSRPAttributes,
|
||||
} from "ente-accounts/services/srp";
|
||||
import { putAttributes } from "ente-accounts/services/user";
|
||||
import { putUserKeyAttributes } from "ente-accounts/services/user";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { LoadingIndicator } from "ente-base/components/loaders";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
@@ -35,7 +35,6 @@ import { useEffect, useState } from "react";
|
||||
const Page: React.FC = () => {
|
||||
const { logout, showMiniDialog } = useBaseContext();
|
||||
|
||||
const [token, setToken] = useState<string>();
|
||||
const [user, setUser] = useState<User>();
|
||||
const [openRecoveryKey, setOpenRecoveryKey] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -59,7 +58,6 @@ const Page: React.FC = () => {
|
||||
} else if (keyAttributes?.encryptedKey) {
|
||||
void router.push("/credentials");
|
||||
} else {
|
||||
setToken(user.token);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
@@ -72,8 +70,7 @@ const Page: React.FC = () => {
|
||||
const { keyAttributes, masterKey, srpSetupAttributes } =
|
||||
await generateKeyAndSRPAttributes(passphrase);
|
||||
|
||||
// TODO: Refactor the code to not require this ensure
|
||||
await putAttributes(token!, keyAttributes);
|
||||
await putUserKeyAttributes(keyAttributes);
|
||||
await configureSRP(srpSetupAttributes);
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
} from "ente-accounts/services/srp-remote";
|
||||
import { getSRPAttributes } from "ente-accounts/services/srp-remote";
|
||||
import {
|
||||
putAttributes,
|
||||
putUserKeyAttributes,
|
||||
sendOTT,
|
||||
verifyEmail,
|
||||
} from "ente-accounts/services/user";
|
||||
@@ -31,6 +31,7 @@ import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { LoadingIndicator } from "ente-base/components/loaders";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
import log from "ente-base/log";
|
||||
import { clearSessionStorage } from "ente-base/session";
|
||||
import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
} from "ente-shared/components/SingleInputForm";
|
||||
@@ -41,7 +42,6 @@ import {
|
||||
getLocalReferralSource,
|
||||
setIsFirstLogin,
|
||||
} from "ente-shared/storage/localStorage/helpers";
|
||||
import { clearKeys } from "ente-shared/storage/sessionStorage";
|
||||
import type { KeyAttributes, User } from "ente-shared/user/types";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -137,11 +137,11 @@ const Page: React.FC = () => {
|
||||
setData("keyAttributes", keyAttributes);
|
||||
setData("originalKeyAttributes", keyAttributes);
|
||||
} else {
|
||||
if (getData("originalKeyAttributes")) {
|
||||
await putAttributes(
|
||||
token!,
|
||||
getData("originalKeyAttributes"),
|
||||
);
|
||||
const originalKeyAttributes = getData(
|
||||
"originalKeyAttributes",
|
||||
);
|
||||
if (originalKeyAttributes) {
|
||||
await putUserKeyAttributes(originalKeyAttributes);
|
||||
}
|
||||
if (getData("srpSetupAttributes")) {
|
||||
const srpSetupAttributes: SRPSetupAttributes =
|
||||
@@ -153,7 +153,7 @@ const Page: React.FC = () => {
|
||||
setIsFirstLogin(true);
|
||||
const redirectURL = unstashRedirect();
|
||||
if (keyAttributes?.encryptedKey) {
|
||||
clearKeys();
|
||||
clearSessionStorage();
|
||||
void router.push(redirectURL ?? "/credentials");
|
||||
} else {
|
||||
void router.push(redirectURL ?? "/generate");
|
||||
|
||||
@@ -2,8 +2,8 @@ import { clearBlobCaches } from "ente-base/blob-cache";
|
||||
import { clearKVDB } from "ente-base/kv";
|
||||
import { clearLocalStorage } from "ente-base/local-storage";
|
||||
import log from "ente-base/log";
|
||||
import { clearSessionStorage } from "ente-base/session";
|
||||
import localForage from "ente-shared/storage/localForage";
|
||||
import { clearKeys } from "ente-shared/storage/sessionStorage";
|
||||
import { clearStashedRedirect } from "./redirect";
|
||||
import { remoteLogoutIfNeeded } from "./user";
|
||||
|
||||
@@ -34,7 +34,7 @@ export const accountLogout = async () => {
|
||||
ignoreError("In-memory store", e);
|
||||
}
|
||||
try {
|
||||
clearKeys();
|
||||
clearSessionStorage();
|
||||
} catch (e) {
|
||||
ignoreError("Session storage", e);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { authenticatedRequestHeaders, HTTPError } from "ente-base/http";
|
||||
import { ensureLocalUser } from "ente-base/local-user";
|
||||
import { ensureLocalUser, getAuthToken } from "ente-base/local-user";
|
||||
import log from "ente-base/log";
|
||||
import { apiURL } from "ente-base/origins";
|
||||
import { getData } from "ente-shared/storage/localStorage";
|
||||
import type { KeyAttributes } from "ente-shared/user/types";
|
||||
import { nullToUndefined } from "ente-utils/transform";
|
||||
import { z } from "zod";
|
||||
import type { SRPAttributes } from "./srp-remote";
|
||||
import { getSRPAttributes } from "./srp-remote";
|
||||
import { putUserKeyAttributes, RemoteKeyAttributes } from "./user";
|
||||
|
||||
type SessionValidity =
|
||||
| { status: "invalid" }
|
||||
@@ -15,6 +19,11 @@ type SessionValidity =
|
||||
updatedSRPAttributes: SRPAttributes;
|
||||
};
|
||||
|
||||
const SessionValidityResponse = z.object({
|
||||
hasSetKeys: z.boolean(),
|
||||
keyAttributes: RemoteKeyAttributes.nullish().transform(nullToUndefined),
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if the local token and/or key attributes we have are still valid.
|
||||
*
|
||||
@@ -72,16 +81,9 @@ export const checkSessionValidity = async (): Promise<SessionValidity> => {
|
||||
|
||||
// See if the response contains keyAttributes (they might not for older
|
||||
// deployments).
|
||||
const json = await res.json();
|
||||
if (
|
||||
"keyAttributes" in json &&
|
||||
typeof json.keyAttributes == "object" &&
|
||||
json.keyAttributes !== null
|
||||
) {
|
||||
// Assume it is a `KeyAttributes`.
|
||||
//
|
||||
// Enhancement: Convert this to a zod validation.
|
||||
const remoteKeyAttributes = json.keyAttributes as KeyAttributes;
|
||||
const { keyAttributes } = SessionValidityResponse.parse(await res.json());
|
||||
if (keyAttributes) {
|
||||
const remoteKeyAttributes = keyAttributes;
|
||||
|
||||
// We should have these values locally if we reach here.
|
||||
const email = ensureLocalUser().email;
|
||||
@@ -106,7 +108,10 @@ export const checkSessionValidity = async (): Promise<SessionValidity> => {
|
||||
// changed.
|
||||
return {
|
||||
status: "validButPasswordChanged",
|
||||
updatedKeyAttributes: remoteKeyAttributes,
|
||||
// TODO:
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
updatedKeyAttributes: remoteKeyAttributes as KeyAttributes,
|
||||
updatedSRPAttributes: remoteSRPAttributes,
|
||||
};
|
||||
}
|
||||
@@ -116,3 +121,51 @@ export const checkSessionValidity = async (): Promise<SessionValidity> => {
|
||||
// The token is still valid (to the best of our ascertainable knowledge).
|
||||
return { status: "valid" };
|
||||
};
|
||||
|
||||
/**
|
||||
* Return `true` if the user does not have a saved auth token, of it is no
|
||||
* longer valid. If needed, also update the key attributes at remote.
|
||||
*
|
||||
* This is a subset of {@link checkSessionValidity} that has been tailored for
|
||||
* use during each remote sync, to detect if the user has been logged out
|
||||
* elsewhere.
|
||||
*
|
||||
* @returns `true` if either we don't have an auth token, or if remote tells us
|
||||
* that the auth token (and the associated session) has been invalidated. In all
|
||||
* other cases, return `false`.
|
||||
*
|
||||
* In particular, this function doesn't throw and instead returns `false` on
|
||||
* errors. This is because returning `true` will trigger a blocking alert that
|
||||
* ends in logging the user out, and we don't want to log the user out on on
|
||||
* e.g. transient network issues.
|
||||
*/
|
||||
export const isSessionInvalid = async (): Promise<boolean> => {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
return true; /* No saved token, session is invalid */
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(await apiURL("/users/session-validity/v2"), {
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status == 401) return true; /* session is no longer valid */
|
||||
else throw new HTTPError(res);
|
||||
}
|
||||
|
||||
const { hasSetKeys } = SessionValidityResponse.parse(await res.json());
|
||||
if (!hasSetKeys) {
|
||||
const originalKeyAttributes = getData("originalKeyAttributes");
|
||||
if (originalKeyAttributes)
|
||||
await putUserKeyAttributes(originalKeyAttributes);
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn("Failed to check session validity", e);
|
||||
// Don't logout user on potentially transient errors.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Everything seems ok.
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -127,7 +127,7 @@ export const verifyEmail = async (
|
||||
/**
|
||||
* Zod schema for {@link KeyAttributes}.
|
||||
*/
|
||||
const RemoteKeyAttributes = z.object({
|
||||
export const RemoteKeyAttributes = z.object({
|
||||
kekSalt: z.string(),
|
||||
encryptedKey: z.string(),
|
||||
keyDecryptionNonce: z.string(),
|
||||
@@ -195,17 +195,17 @@ export type TwoFactorAuthorizationResponse = z.infer<
|
||||
typeof TwoFactorAuthorizationResponse
|
||||
>;
|
||||
|
||||
export const putAttributes = async (
|
||||
token: string,
|
||||
keyAttributes: KeyAttributes,
|
||||
) =>
|
||||
HTTPService.put(
|
||||
await apiURL("/users/attributes"),
|
||||
{ keyAttributes },
|
||||
undefined,
|
||||
{ "X-Auth-Token": token },
|
||||
/**
|
||||
* Update or set the user's {@link KeyAttributes} on remote.
|
||||
*/
|
||||
export const putUserKeyAttributes = async (keyAttributes: KeyAttributes) =>
|
||||
ensureOk(
|
||||
await fetch(await apiURL("/users/attributes"), {
|
||||
method: "PUT",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify({ keyAttributes }),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Log the user out on remote, if possible and needed.
|
||||
*/
|
||||
|
||||
@@ -47,19 +47,25 @@ export const ensureLocalUser = (): LocalUser => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the user's auth token, or throw an error.
|
||||
* Return the user's auth token, if present.
|
||||
*
|
||||
* The user's auth token is stored in KV DB after they have successfully logged
|
||||
* in. This function returns that saved auth token.
|
||||
*
|
||||
* If no such token is found (which should only happen if the user is not logged
|
||||
* in), then it throws an error.
|
||||
*
|
||||
* The underlying data is stored in IndexedDB, and can be accessed from web
|
||||
* workers.
|
||||
*/
|
||||
export const getAuthToken = () => getKVS("token");
|
||||
|
||||
/**
|
||||
* Return the user's auth token, or throw an error.
|
||||
*
|
||||
* The user's auth token can be retrieved using {@link getAuthToken}. This
|
||||
* function is a wrapper which throws an error if the token is not found (which
|
||||
* should only happen if the user is not logged in).
|
||||
*/
|
||||
export const ensureAuthToken = async () => {
|
||||
const token = await getKVS("token");
|
||||
const token = await getAuthToken();
|
||||
if (!token) throw new Error("Not logged in");
|
||||
return token;
|
||||
};
|
||||
|
||||
@@ -675,5 +675,7 @@
|
||||
"theme": "Theme",
|
||||
"system": "System",
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
"dark": "Dark",
|
||||
"streamable_videos": "Streamable videos",
|
||||
"processing_videos_status": "Processing videos..."
|
||||
}
|
||||
|
||||
@@ -2,6 +2,47 @@ import { z } from "zod";
|
||||
import { decryptBox } from "./crypto";
|
||||
import { toB64 } from "./crypto/libsodium";
|
||||
|
||||
/**
|
||||
* Remove all data stored in session storage (data tied to the browser tab).
|
||||
*
|
||||
* See `docs/storage.md` for more details about session storage. Currently, only
|
||||
* the following entries are stored in session storage:
|
||||
*
|
||||
* - "encryptionKey"
|
||||
* - "keyEncryptionKey" (transient)
|
||||
*/
|
||||
export const clearSessionStorage = () => sessionStorage.clear();
|
||||
|
||||
/**
|
||||
* Schema of JSON string value for the "encryptionKey" and "keyEncryptionKey"
|
||||
* keys strings stored in session storage.
|
||||
*/
|
||||
const SessionKeyData = z.object({
|
||||
encryptedData: z.string(),
|
||||
key: z.string(),
|
||||
nonce: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Save the user's encrypted master key in the session storage.
|
||||
*
|
||||
* @param keyB64 The user's master key as a base64 encoded string.
|
||||
*/
|
||||
// TODO(RE):
|
||||
// export const saveMasterKeyInSessionStore = async (
|
||||
// keyB64: string,
|
||||
// fromDesktop?: boolean,
|
||||
// ) => {
|
||||
// const cryptoWorker = await sharedCryptoWorker();
|
||||
// const sessionKeyAttributes =
|
||||
// await cryptoWorker.generateKeyAndEncryptToB64(key);
|
||||
// setKey(keyType, sessionKeyAttributes);
|
||||
// const electron = globalThis.electron;
|
||||
// if (electron && !fromDesktop) {
|
||||
// electron.saveMasterKeyB64(key);
|
||||
// }
|
||||
// };
|
||||
|
||||
/**
|
||||
* Return the user's decrypted master key from session storage.
|
||||
*
|
||||
@@ -37,7 +78,7 @@ export const masterKeyFromSessionIfLoggedIn = async () => {
|
||||
const value = sessionStorage.getItem("encryptionKey");
|
||||
if (!value) return undefined;
|
||||
|
||||
const { encryptedData, key, nonce } = EncryptionKeyAttributes.parse(
|
||||
const { encryptedData, key, nonce } = SessionKeyData.parse(
|
||||
JSON.parse(value),
|
||||
);
|
||||
return decryptBox({ encryptedData, nonce }, key);
|
||||
@@ -49,9 +90,31 @@ export const masterKeyFromSessionIfLoggedIn = async () => {
|
||||
*/
|
||||
export const masterKeyB64FromSession = () => masterKeyFromSession().then(toB64);
|
||||
|
||||
// TODO: Same as B64EncryptionResult. Revisit.
|
||||
const EncryptionKeyAttributes = z.object({
|
||||
encryptedData: z.string(),
|
||||
key: z.string(),
|
||||
nonce: z.string(),
|
||||
});
|
||||
/**
|
||||
* Return the decrypted user's key encryption key ("kek") from session storage
|
||||
* if present, otherwise return `undefined`.
|
||||
*
|
||||
* [Note: Stashing kek in session store]
|
||||
*
|
||||
* During login, if the user has set a second factor (passkey or TOTP), then we
|
||||
* need to redirect them to the accounts app or TOTP page to verify the second
|
||||
* factor. This second factor verification happens after password verification,
|
||||
* but simply storing the decrypted kek in-memory wouldn't work because the
|
||||
* second factor redirect can happen to a separate accounts app altogether.
|
||||
*
|
||||
* So instead, we stash the encrypted kek in session store (using
|
||||
* {@link stashKeyEncryptionKeyInSessionStore}), and after redirect, retrieve
|
||||
* it (after clearing it) using {@link unstashKeyEncryptionKeyFromSession}.
|
||||
*/
|
||||
export const unstashKeyEncryptionKeyFromSession = async () => {
|
||||
// TODO: Same value as the deprecated getKey("keyEncryptionKey")
|
||||
const value = sessionStorage.getItem("keyEncryptionKey");
|
||||
if (!value) return undefined;
|
||||
|
||||
sessionStorage.removeItem("keyEncryptionKey");
|
||||
|
||||
const { encryptedData, key, nonce } = SessionKeyData.parse(
|
||||
JSON.parse(value),
|
||||
);
|
||||
return decryptBox({ encryptedData, nonce }, key);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { EnteLogo, EnteLogoBox } from "ente-base/components/EnteLogo";
|
||||
import type { ButtonishProps } from "ente-base/components/mui";
|
||||
import { useIsSmallWidth } from "ente-base/components/utils/hooks";
|
||||
import { pt } from "ente-base/i18n";
|
||||
import {
|
||||
hlsGenerationStatusSnapshot,
|
||||
isHLSGenerationSupported,
|
||||
@@ -430,8 +429,7 @@ const EmptyState: React.FC<
|
||||
case "done":
|
||||
// If ML is not running, see if video processing is.
|
||||
if (vpStatus?.enabled && vpStatus.status == "processing") {
|
||||
// TODO(HLS):
|
||||
label = pt("Processing videos...");
|
||||
label = t("processing_videos_status");
|
||||
}
|
||||
break;
|
||||
case "scheduled":
|
||||
|
||||
@@ -10,10 +10,32 @@
|
||||
* is a needed for fast refresh to work.
|
||||
*/
|
||||
|
||||
import log from "ente-base/log";
|
||||
import type { Collection } from "ente-media/collection";
|
||||
import type { FamilyData } from "ente-new/photos/services/user-details";
|
||||
import { getRecoveryKey } from "ente-shared/crypto/helpers";
|
||||
import type { User } from "ente-shared/user/types";
|
||||
|
||||
/**
|
||||
* Ensure that the keys in local storage are not malformed by verifying that the
|
||||
* recoveryKey can be decrypted with the masterKey.
|
||||
*
|
||||
* This is not meant to be bullet proof, but more like an extra sanity check.
|
||||
*
|
||||
* @returns `true` if the sanity check passed, otherwise `false`. Since failure
|
||||
* is not expected, the caller should {@link logout} on `false` to avoid
|
||||
* continuing with an unexpected local state.
|
||||
*/
|
||||
export const validateKey = async () => {
|
||||
try {
|
||||
await getRecoveryKey();
|
||||
return true;
|
||||
} catch (e) {
|
||||
log.warn("Failed to validate key" /*, caller will logout */, e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const constructUserIDToEmailMap = (
|
||||
user: User,
|
||||
collections: Collection[],
|
||||
|
||||
@@ -65,7 +65,7 @@ export const setLSUser = async (user: object) => {
|
||||
export const migrateKVToken = async (user: unknown) => {
|
||||
// Throw an error if the data is in local storage but not in IndexedDB. This
|
||||
// is a pre-cursor to inlining this code.
|
||||
// TODO(REL): Remove this sanity check after a few days.
|
||||
// TODO: Remove this sanity check eventually when this code is revisited.
|
||||
const oldLSUser = getData("user");
|
||||
const wasMissing =
|
||||
oldLSUser &&
|
||||
|
||||
@@ -9,5 +9,3 @@ export const getKey = (key: SessionKey) => {
|
||||
};
|
||||
|
||||
export const removeKey = (key: SessionKey) => sessionStorage.removeItem(key);
|
||||
|
||||
export const clearKeys = () => sessionStorage.clear();
|
||||
|
||||
Reference in New Issue
Block a user