Split
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import "@fontsource-variable/inter";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { getData } from "ente-accounts/services/accounts-db";
|
||||
import { savedLocalUser } from "ente-accounts/services/accounts-db";
|
||||
import { accountLogout } from "ente-accounts/services/logout";
|
||||
import type { User } from "ente-accounts/services/user";
|
||||
import { staticAppTitle } from "ente-base/app";
|
||||
import { CustomHead } from "ente-base/components/Head";
|
||||
import {
|
||||
@@ -32,8 +31,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog();
|
||||
|
||||
useEffect(() => {
|
||||
const user = getData("user") as User | undefined | null;
|
||||
logStartupBanner(user?.id);
|
||||
logStartupBanner(savedLocalUser()?.id);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useNotification } from "components/utils/hooks-app";
|
||||
import {
|
||||
getData,
|
||||
isLocalStorageAndIndexedDBMismatch,
|
||||
savedLocalUser,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import type { User } from "ente-accounts/services/user";
|
||||
import { isDesktop, staticAppTitle } from "ente-base/app";
|
||||
import { CenteredRow } from "ente-base/components/containers";
|
||||
import { CustomHead } from "ente-base/components/Head";
|
||||
@@ -68,8 +68,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
const logout = useCallback(() => void photosLogout(), []);
|
||||
|
||||
useEffect(() => {
|
||||
const user = getData("user") as User | undefined | null;
|
||||
logStartupBanner(user?.id);
|
||||
logStartupBanner(savedLocalUser()?.id);
|
||||
void isLocalStorageAndIndexedDBMismatch().then((mismatch) => {
|
||||
if (mismatch) {
|
||||
log.error("Logging out (IndexedDB and local storage mismatch)");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getData } from "ente-accounts/services/accounts-db";
|
||||
import type { LocalUser, User } from "ente-accounts/services/user";
|
||||
import type { LocalUser, PartialLocalUser } from "ente-accounts/services/user";
|
||||
import { joinPath } from "ente-base/file-name";
|
||||
import log from "ente-base/log";
|
||||
import { type Electron } from "ente-base/types/ipc";
|
||||
@@ -284,7 +284,7 @@ export const getArchivedFiles = (files: EnteFile[]) => {
|
||||
};
|
||||
|
||||
export const getUserOwnedFiles = (files: EnteFile[]) => {
|
||||
const user: User = getData("user");
|
||||
const user: PartialLocalUser = getData("user");
|
||||
if (!user?.id) {
|
||||
throw Error("user missing");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Input, Stack, TextField, Typography } from "@mui/material";
|
||||
import { AccountsPageFooter } from "ente-accounts/components/layouts/centered-paper";
|
||||
import { saveSRPAttributes } from "ente-accounts/services/accounts-db";
|
||||
import {
|
||||
savePartialLocalUser,
|
||||
saveSRPAttributes,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import { getSRPAttributes } from "ente-accounts/services/srp";
|
||||
import { savePartialLocalUser, sendOTT } from "ente-accounts/services/user";
|
||||
import { sendOTT } from "ente-accounts/services/user";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { LoadingButton } from "ente-base/components/mui/LoadingButton";
|
||||
import { isMuseumHTTPError } from "ente-base/http";
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import {
|
||||
saveJustSignedUp,
|
||||
saveOriginalKeyAttributes,
|
||||
savePartialLocalUser,
|
||||
stashReferralSource,
|
||||
stashSRPSetupAttributes,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
@@ -23,7 +24,6 @@ import { generateSRPSetupAttributes } from "ente-accounts/services/srp";
|
||||
import {
|
||||
generateAndSaveInteractiveKeyAttributes,
|
||||
generateKeysAndAttributes,
|
||||
savePartialLocalUser,
|
||||
sendOTT,
|
||||
type GenerateKeysAndAttributesResult,
|
||||
} from "ente-accounts/services/user";
|
||||
|
||||
@@ -5,11 +5,7 @@ import {
|
||||
AccountsPageTitle,
|
||||
} from "ente-accounts/components/layouts/centered-paper";
|
||||
import { appHomeRoute, stashRedirect } from "ente-accounts/services/redirect";
|
||||
import {
|
||||
changePassword,
|
||||
localUser,
|
||||
type LocalUser,
|
||||
} from "ente-accounts/services/user";
|
||||
import { changePassword, type LocalUser } from "ente-accounts/services/user";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { LoadingIndicator } from "ente-base/components/loaders";
|
||||
import { deriveKeyInsufficientMemoryErrorMessage } from "ente-base/crypto/types";
|
||||
@@ -21,12 +17,13 @@ import {
|
||||
NewPasswordForm,
|
||||
type NewPasswordFormProps,
|
||||
} from "../components/NewPasswordForm";
|
||||
import { savedLocalUser } from "../services/accounts-db";
|
||||
|
||||
/**
|
||||
* A page that allows a user to reset or change their password.
|
||||
*/
|
||||
const Page: React.FC = () => {
|
||||
const [user, setUser] = useState<LocalUser>();
|
||||
const [user, setUser] = useState<LocalUser | undefined>(undefined);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -34,7 +31,7 @@ const Page: React.FC = () => {
|
||||
const isReset = router.query.op == "reset";
|
||||
|
||||
useEffect(() => {
|
||||
const user = localUser();
|
||||
const user = savedLocalUser();
|
||||
if (user) {
|
||||
setUser(user);
|
||||
} else {
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
import {
|
||||
generateAndSaveInteractiveKeyAttributes,
|
||||
type KeyAttributes,
|
||||
type User,
|
||||
type PartialLocalUser,
|
||||
} from "ente-accounts/services/user";
|
||||
import { decryptAndStoreToken } from "ente-accounts/utils/helpers";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
@@ -81,7 +81,7 @@ const Page: React.FC = () => {
|
||||
|
||||
const [srpAttributes, setSrpAttributes] = useState<SRPAttributes>();
|
||||
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
|
||||
const [user, setUser] = useState<User>();
|
||||
const [user, setUser] = useState<PartialLocalUser>();
|
||||
const [passkeyVerificationData, setPasskeyVerificationData] = useState<
|
||||
{ passkeySessionID: string; url: string } | undefined
|
||||
>();
|
||||
@@ -127,7 +127,7 @@ const Page: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
const user: User = getData("user");
|
||||
const user: PartialLocalUser = getData("user");
|
||||
if (!user?.email) {
|
||||
void router.push("/");
|
||||
return;
|
||||
@@ -280,7 +280,7 @@ const Page: React.FC = () => {
|
||||
await decryptAndStoreToken(keyAttributes, masterKey);
|
||||
try {
|
||||
let srpAttributes = savedSRPAttributes();
|
||||
if (!srpAttributes && user) {
|
||||
if (!srpAttributes && user?.email) {
|
||||
srpAttributes = await getSRPAttributes(user.email);
|
||||
if (srpAttributes) {
|
||||
saveSRPAttributes(srpAttributes);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
generateSRPSetupAttributes,
|
||||
setupSRP,
|
||||
} from "ente-accounts/services/srp";
|
||||
import type { User } from "ente-accounts/services/user";
|
||||
import type { PartialLocalUser } from "ente-accounts/services/user";
|
||||
import {
|
||||
generateAndSaveInteractiveKeyAttributes,
|
||||
generateKeysAndAttributes,
|
||||
@@ -42,14 +42,14 @@ import {
|
||||
const Page: React.FC = () => {
|
||||
const { logout, showMiniDialog } = useBaseContext();
|
||||
|
||||
const [user, setUser] = useState<User>();
|
||||
const [user, setUser] = useState<PartialLocalUser>();
|
||||
const [openRecoveryKey, setOpenRecoveryKey] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const user: User = getData("user");
|
||||
const user: PartialLocalUser = getData("user");
|
||||
setUser(user);
|
||||
if (!user?.token) {
|
||||
void router.push("/");
|
||||
@@ -109,7 +109,7 @@ const Page: React.FC = () => {
|
||||
<AccountsPageContents>
|
||||
<AccountsPageTitle>{t("set_password")}</AccountsPageTitle>
|
||||
<NewPasswordForm
|
||||
userEmail={user.email}
|
||||
userEmail={user.email! /* TODO(RE):*/}
|
||||
submitButtonTitle={t("set_password")}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import { recoveryKeyFromMnemonic } from "ente-accounts/services/recovery-key";
|
||||
import { appHomeRoute, stashRedirect } from "ente-accounts/services/redirect";
|
||||
import type { KeyAttributes, User } from "ente-accounts/services/user";
|
||||
import type {
|
||||
KeyAttributes,
|
||||
PartialLocalUser,
|
||||
} from "ente-accounts/services/user";
|
||||
import { sendOTT } from "ente-accounts/services/user";
|
||||
import { decryptAndStoreToken } from "ente-accounts/utils/helpers";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
@@ -38,7 +41,7 @@ const Page: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const user: User = getData("user");
|
||||
const user: PartialLocalUser = getData("user");
|
||||
if (!user?.email) {
|
||||
void router.push("/");
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
saveKeyAttributes,
|
||||
setLSUser,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import type { User } from "ente-accounts/services/user";
|
||||
import type { PartialLocalUser } from "ente-accounts/services/user";
|
||||
import { verifyTwoFactor } from "ente-accounts/services/user";
|
||||
import { LinkButton } from "ente-base/components/LinkButton";
|
||||
import { useBaseContext } from "ente-base/context";
|
||||
@@ -27,7 +27,7 @@ const Page: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const user: User = getData("user");
|
||||
const user: PartialLocalUser = getData("user");
|
||||
if (!user?.email || !user.twoFactorSessionID) {
|
||||
void router.push("/");
|
||||
} else if (
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
unstashRedirect,
|
||||
} from "ente-accounts/services/redirect";
|
||||
import { getSRPAttributes, setupSRP } from "ente-accounts/services/srp";
|
||||
import type { User } from "ente-accounts/services/user";
|
||||
import type { PartialLocalUser } from "ente-accounts/services/user";
|
||||
import {
|
||||
putUserKeyAttributes,
|
||||
sendOTT,
|
||||
@@ -69,7 +69,7 @@ const Page: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
const user: User = getData("user");
|
||||
const user: PartialLocalUser = getData("user");
|
||||
|
||||
const redirect = await redirectionIfNeeded(user);
|
||||
if (redirect) {
|
||||
@@ -253,7 +253,7 @@ export default Page;
|
||||
*
|
||||
* @returns The slug to redirect to, if needed.
|
||||
*/
|
||||
const redirectionIfNeeded = async (user: User | undefined) => {
|
||||
const redirectionIfNeeded = async (user: PartialLocalUser | undefined) => {
|
||||
const email = user?.email;
|
||||
if (!email) {
|
||||
return "/";
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
import { getKVS, removeKV, setKV } from "ente-base/kv";
|
||||
import log from "ente-base/log";
|
||||
import { nullToUndefined } from "ente-utils/transform";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
RemoteSRPAttributes,
|
||||
SRPSetupAttributes,
|
||||
type SRPAttributes,
|
||||
} from "./srp";
|
||||
import { RemoteKeyAttributes, type KeyAttributes } from "./user";
|
||||
|
||||
export type LocalStorageKey = "user";
|
||||
|
||||
/**
|
||||
* [Note: Accounts DB]
|
||||
* @file [Note: Accounts DB]
|
||||
*
|
||||
* The accounts package stores various state both during the login / signup
|
||||
* flow, and post login to identify the logged in user.
|
||||
@@ -29,6 +16,145 @@ export type LocalStorageKey = "user";
|
||||
* - "originalKeyAttributes"
|
||||
* - "srpAttributes"
|
||||
*/
|
||||
|
||||
import { getKVS, removeKV, setKV } from "ente-base/kv";
|
||||
import log from "ente-base/log";
|
||||
import { getAuthToken } from "ente-base/token";
|
||||
import { nullToUndefined } from "ente-utils/transform";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
RemoteSRPAttributes,
|
||||
SRPSetupAttributes,
|
||||
type SRPAttributes,
|
||||
} from "./srp";
|
||||
import {
|
||||
RemoteKeyAttributes,
|
||||
type KeyAttributes,
|
||||
type LocalUser,
|
||||
} from "./user";
|
||||
|
||||
/**
|
||||
* The local storage data about the user before login or signup is complete.
|
||||
*
|
||||
* [Note: Partial local user]
|
||||
*
|
||||
* During login or signup, the user object exists in various partial states in
|
||||
* local storage.
|
||||
*
|
||||
* - Initially, there is no user object in local storage.
|
||||
*
|
||||
* - When the user enters their email, the email property of the stored object
|
||||
* is set, but nothing else.
|
||||
*
|
||||
* - After they verify their password, we have two cases: if second factor
|
||||
* verification is not set, and when it is set.
|
||||
*
|
||||
* - If second factor verification is not set, then after verifying their
|
||||
* password their {@link id} and {@link encryptedToken} will get filled in,
|
||||
* and {@link isTwoFactorEnabled} will be set to false.
|
||||
*
|
||||
* - If they have second factor verification set, then after verifying their
|
||||
* password {@link isTwoFactorEnabled} and {@link twoFactorSessionID} will
|
||||
* also get filled in. Once they verify their TOTP based second factor, their
|
||||
* {@link id} and {@link encryptedToken} will also get filled in.
|
||||
*
|
||||
* - As the login or signup sequence completes, a {@link token} obtained from
|
||||
* the {@link encryptedToken} will be written out, and the
|
||||
* {@link encryptedToken} cleared since it is not needed anymore.
|
||||
*
|
||||
* So while the underlying storage is the same, we offer two APIs for code to
|
||||
* obtain the user:
|
||||
*
|
||||
* - Before login is complete, or when it is unknown if login is complete or
|
||||
* not, then {@link savedPartialLocalUser} can be used to obtain a
|
||||
* {@link PartialLocalUser} with all of its properties set to be optional (and
|
||||
* some additional properties not available in the regular user object).
|
||||
*
|
||||
* - When we know that the login has completed, we can use either
|
||||
* {@link savedLocalUser} (which returns `undefined` if our assumption is
|
||||
* false) or {@link ensureSavedLocalUser} (which throws if our assumption is
|
||||
* false) to obtain an object with all the properties expected to be present
|
||||
* for a locally persisted user set to be required.
|
||||
*/
|
||||
export interface PartialLocalUser {
|
||||
id?: number;
|
||||
email?: string;
|
||||
token?: string;
|
||||
encryptedToken?: string;
|
||||
isTwoFactorEnabled?: boolean;
|
||||
twoFactorSessionID?: string;
|
||||
}
|
||||
|
||||
const PartialLocalUser = z.object({
|
||||
id: z.number().nullish().transform(nullToUndefined),
|
||||
email: z.string().nullish().transform(nullToUndefined),
|
||||
token: z.string().nullish().transform(nullToUndefined),
|
||||
encryptedToken: z.string().nullish().transform(nullToUndefined),
|
||||
isTwoFactorEnabled: z.boolean().nullish().transform(nullToUndefined),
|
||||
twoFactorSessionID: z.string().nullish().transform(nullToUndefined),
|
||||
});
|
||||
|
||||
/**
|
||||
* Zod schema for the {@link LocalUser} TypeScript type.
|
||||
*
|
||||
* The type itself is in `user.ts`.
|
||||
*/
|
||||
const LocalUser = z.object({
|
||||
id: z.number(),
|
||||
email: z.string(),
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Return the local storage value of the user's data.
|
||||
*
|
||||
* This function is meant to be called during the login or signup sequence.
|
||||
* After the user is logged in, use {@link savedLocalUser} or
|
||||
* {@link ensureLocalUser} instead.
|
||||
*
|
||||
* Use {@link savePartialLocalUser} to updated the saved value.
|
||||
*/
|
||||
export const savedPartialLocalUser = (): PartialLocalUser | undefined => {
|
||||
const jsonString = localStorage.getItem("user");
|
||||
if (!jsonString) return undefined;
|
||||
return PartialLocalUser.parse(JSON.parse(jsonString));
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the users data as we accrue it during the signup or login flow.
|
||||
*
|
||||
* See: [Note: Partial local user].
|
||||
*
|
||||
* TODO: WARNING: This does not update the KV token. The idea is to gradually
|
||||
* move over uses of setLSUser to this while explicitly setting the KV token
|
||||
* where needed.
|
||||
*/
|
||||
export const savePartialLocalUser = (partialLocalUser: Partial<LocalUser>) =>
|
||||
localStorage.setItem("user", JSON.stringify(partialLocalUser));
|
||||
|
||||
/**
|
||||
* Return data about the logged-in user, if someone is indeed logged in.
|
||||
* Otherwise return `undefined`.
|
||||
*
|
||||
* The user's data is stored in the browser's localStorage. Thus, this function
|
||||
* only works from the main thread, not from web workers since local storage is
|
||||
* not accessible to web workers.
|
||||
*
|
||||
* There is no setter corresponding to this function since this is only a view
|
||||
* on data saved using {@link savePartialLocalUser}.
|
||||
*
|
||||
* See: [Note: Partial local user] for more about the whole shebang.
|
||||
*/
|
||||
export const savedLocalUser = (): LocalUser | undefined => {
|
||||
const jsonString = localStorage.getItem("user");
|
||||
if (!jsonString) return undefined;
|
||||
// We might have some data, but not all of it. So do a non-throwing parse.
|
||||
const { success, data } = LocalUser.safeParse(JSON.parse(jsonString));
|
||||
return success ? data : undefined;
|
||||
};
|
||||
|
||||
export type LocalStorageKey = "user";
|
||||
|
||||
export const getData = (key: LocalStorageKey) => {
|
||||
try {
|
||||
if (
|
||||
@@ -110,16 +236,8 @@ export const migrateKVToken = async (user: unknown) => {
|
||||
* This acts a sanity check on IndexedDB by ensuring that if the user has a
|
||||
* token in local storage, then it should also be present in IndexedDB.
|
||||
*/
|
||||
export const isLocalStorageAndIndexedDBMismatch = async () => {
|
||||
const oldLSUser = getData("user");
|
||||
return (
|
||||
oldLSUser &&
|
||||
typeof oldLSUser == "object" &&
|
||||
"token" in oldLSUser &&
|
||||
typeof oldLSUser.token == "string" &&
|
||||
!(await getKVS("token"))
|
||||
);
|
||||
};
|
||||
export const isLocalStorageAndIndexedDBMismatch = async () =>
|
||||
savedPartialLocalUser()?.token && !(await getAuthToken());
|
||||
|
||||
/**
|
||||
* Return the user's {@link KeyAttributes} if they are present in local storage.
|
||||
@@ -155,6 +273,9 @@ export const saveKeyAttributes = (keyAttributes: KeyAttributes) =>
|
||||
* either freshly generated (if the user signed up on this client) or were
|
||||
* fetched from remote (otherwise).
|
||||
*
|
||||
* > NOTE: Currently the code does not guarantee that savedOriginalKeyAttributes
|
||||
* > will always be set when savedKeyAttributes is set.
|
||||
*
|
||||
* In contrast, the regular key attributes get overwritten by the local only
|
||||
* interactive key attributes for the user's convenience. See the documentation
|
||||
* of {@link generateAndSaveInteractiveKeyAttributes} for more details.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
getData,
|
||||
savedKeyAttributes,
|
||||
savedLocalUser,
|
||||
saveKeyAttributes,
|
||||
saveSRPAttributes,
|
||||
setLSUser,
|
||||
type PartialLocalUser,
|
||||
} from "ente-accounts/services/accounts-db";
|
||||
import {
|
||||
generateSRPSetupAttributes,
|
||||
@@ -36,121 +38,51 @@ import { nullToUndefined } from "ente-utils/transform";
|
||||
import { z } from "zod/v4";
|
||||
import { getUserRecoveryKey, recoveryKeyFromMnemonic } from "./recovery-key";
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
token: string;
|
||||
encryptedToken: string;
|
||||
isTwoFactorEnabled: boolean;
|
||||
twoFactorSessionID: string;
|
||||
}
|
||||
// TODO(RE): Temporary re-export
|
||||
export type { PartialLocalUser };
|
||||
|
||||
/**
|
||||
* The local storage data about the user after they've logged in.
|
||||
* The locally persisted data we have about the user after they've logged in.
|
||||
*
|
||||
* This type arguably belongs to accounts-db (since that's what persists it and
|
||||
* its shadow alias, {@link PartialLocalUser}), but since most code that will
|
||||
* need this will need this after login has completed, and will be using the
|
||||
* {@link ensureLocalUser} method below, we keep this in the same file to reduce
|
||||
* the need to import the type from a separate file.
|
||||
*/
|
||||
const LocalUser = z.object({
|
||||
export interface LocalUser {
|
||||
/**
|
||||
* The user's ID.
|
||||
*/
|
||||
id: z.number(),
|
||||
id: number;
|
||||
/**
|
||||
* The user's email.
|
||||
* The email associated with the user's Ente account.
|
||||
*/
|
||||
email: z.string(),
|
||||
email: string;
|
||||
/**
|
||||
* The user's (plaintext) auth token.
|
||||
*
|
||||
* It is used for making API calls on their behalf, by passing this token as
|
||||
* the value of the X-Auth-Token header in the HTTP request.
|
||||
*
|
||||
* Deprecated, use `getAuthToken()` instead (which fetches it from IDB).
|
||||
* Usually you shouldn't be needing to access this property; instead use
|
||||
* {@link getAuthToken()} which is kept in sync with this value, and lives
|
||||
* in IndexedDB and thus can also be used in web workers.
|
||||
*/
|
||||
token: z.string(),
|
||||
});
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The local storage data about the user after they've logged in.
|
||||
*/
|
||||
export type LocalUser = z.infer<typeof LocalUser>;
|
||||
|
||||
/**
|
||||
* The local storage data about the user before login or signup is complete.
|
||||
* Return the currently logged in {@link LocalUser}, throwing it the user is not
|
||||
* logged in.
|
||||
*
|
||||
* [Note: Partial local user]
|
||||
*
|
||||
* During login or signup, the user object exists in various partial states in
|
||||
* local storage.
|
||||
*
|
||||
* - Initially, there is no user object in local storage.
|
||||
*
|
||||
* - When the user enters their email, the email property of the stored object
|
||||
* is set, but nothing else.
|
||||
*
|
||||
* - After they verify their password, we have two cases: if second factor
|
||||
* verification is not set, and when it is set.
|
||||
*
|
||||
* - If second factor verification is not set, then after verifying their
|
||||
* password their {@link id} and {@link encryptedToken} will get filled in,
|
||||
* and {@link isTwoFactorEnabled} will be set to false.
|
||||
*
|
||||
* - If they have second factor verification set, then after verifying their
|
||||
* password {@link isTwoFactorEnabled} and {@link twoFactorSessionID} will
|
||||
* also get filled in. Once they verify their TOTP based second factor, their
|
||||
* {@link id} and {@link encryptedToken} will also get filled in.
|
||||
*
|
||||
* So while the underlying storage is the same, we offer two APIs for code to
|
||||
* obtain the user:
|
||||
*
|
||||
* - Before login is complete, or when it is unknown if login is complete or
|
||||
* not, then {@link partialLocalUser} can be used to obtain a
|
||||
* {@link LocalUser} with all of its properties set to be optional.
|
||||
*
|
||||
* - When we know that the login has completed, we can use either
|
||||
* {@link localUser} (which returns `undefined` if our presumption is false)
|
||||
* or {@link ensureLocalUser} (which throws if our presumption is false) to
|
||||
* obtain an object with all the properties expected to be present for a
|
||||
* locally persisted user set to be required.
|
||||
*/
|
||||
export const partialLocalUser = (): Partial<LocalUser> | undefined => {
|
||||
// TODO: duplicate of getData("user")
|
||||
const s = localStorage.getItem("user");
|
||||
if (!s) return undefined;
|
||||
return LocalUser.partial().parse(JSON.parse(s));
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the users data as we accrue it during the signup or login flow.
|
||||
*
|
||||
* See: [Note: Partial local user].
|
||||
*
|
||||
* TODO: WARNING: This does not update the KV token. The idea is to gradually
|
||||
* move over uses of setLSUser to this while explicitly setting the KV token
|
||||
* where needed.
|
||||
*/
|
||||
export const savePartialLocalUser = (partialLocalUser: Partial<LocalUser>) =>
|
||||
localStorage.setItem("user", JSON.stringify(partialLocalUser));
|
||||
|
||||
/**
|
||||
* Return the logged-in user, if someone is indeed logged in. Otherwise return
|
||||
* `undefined`.
|
||||
*
|
||||
* The user's data is stored in the browser's localStorage. Thus, this function
|
||||
* only works from the main thread, not from web workers (local storage is not
|
||||
* accessible to web workers).
|
||||
*/
|
||||
export const localUser = (): LocalUser | undefined => {
|
||||
// TODO: duplicate of getData("user")
|
||||
const s = localStorage.getItem("user");
|
||||
if (!s) return undefined;
|
||||
const { success, data } = LocalUser.safeParse(JSON.parse(s));
|
||||
return success ? data : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper over {@link localUser} with that throws if no one is logged in.
|
||||
* This is a wrapper over the {@link savedLocalUser} function that throws if no
|
||||
* one is logged in. A more appropriate name for this function, keeping in line
|
||||
* with the conventions the other methods follow, would've been
|
||||
* {@link ensureSavedLocalUser}. The shorter name is for readability.
|
||||
*/
|
||||
export const ensureLocalUser = (): LocalUser =>
|
||||
ensureExpectedLoggedInValue(localUser());
|
||||
ensureExpectedLoggedInValue(savedLocalUser());
|
||||
|
||||
/**
|
||||
* A function throws an error if a value that is expected to be truthy when the
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { User } from "ente-accounts/services/user";
|
||||
import { type LocalUser } from "ente-accounts/services/user";
|
||||
import {
|
||||
groupFilesByCollectionID,
|
||||
sortFiles,
|
||||
@@ -108,15 +108,15 @@ export interface GalleryState {
|
||||
/*--< Mostly static state >--*/
|
||||
|
||||
/**
|
||||
* The logged in {@link User}.
|
||||
* The logged in {@link LocalUser}.
|
||||
*
|
||||
* This is expected to be undefined only for a brief duration until the code
|
||||
* for the initial "mount" runs (If we're not logged in, then the gallery
|
||||
* will redirect the user to an appropriate authentication page).
|
||||
*/
|
||||
user: User | undefined;
|
||||
user: LocalUser | undefined;
|
||||
/**
|
||||
* Family plan related information for the logged in {@link User}.
|
||||
* Family plan related information for the logged in {@link LocalUser}.
|
||||
*/
|
||||
familyData: FamilyData | undefined;
|
||||
|
||||
@@ -449,7 +449,7 @@ export interface GalleryState {
|
||||
export type GalleryAction =
|
||||
| {
|
||||
type: "mount";
|
||||
user: User;
|
||||
user: LocalUser;
|
||||
familyData: FamilyData | undefined;
|
||||
collections: Collection[];
|
||||
collectionFiles: EnteFile[];
|
||||
@@ -1225,7 +1225,7 @@ const deriveArchivedFileIDs = (
|
||||
* Compute favorite file IDs from their dependencies.
|
||||
*/
|
||||
const deriveFavoriteFileIDs = (
|
||||
user: User,
|
||||
user: LocalUser,
|
||||
collections: GalleryState["collections"],
|
||||
collectionFiles: GalleryState["collectionFiles"],
|
||||
unsyncedFavoriteUpdates: GalleryState["unsyncedFavoriteUpdates"],
|
||||
@@ -1278,7 +1278,7 @@ const createFileCollectionIDs = (files: EnteFile[]) =>
|
||||
*/
|
||||
const deriveNormalCollectionSummaries = (
|
||||
normalCollections: Collection[],
|
||||
user: User,
|
||||
user: LocalUser,
|
||||
trashItems: GalleryState["trashItems"],
|
||||
collectionFiles: GalleryState["collectionFiles"],
|
||||
hiddenFileIDs: GalleryState["hiddenFileIDs"],
|
||||
@@ -1378,7 +1378,7 @@ const pseudoCollectionOptionsForLatestFileAndCount = (
|
||||
*/
|
||||
const deriveHiddenCollectionSummaries = (
|
||||
hiddenCollections: Collection[],
|
||||
user: User,
|
||||
user: LocalUser,
|
||||
collectionFiles: GalleryState["collectionFiles"],
|
||||
) => {
|
||||
const hiddenCollectionSummaries = createCollectionSummaries(
|
||||
@@ -1414,7 +1414,7 @@ const deriveUncategorizedCollectionSummaryID = (
|
||||
PseudoCollectionID.uncategorizedPlaceholder;
|
||||
|
||||
const createCollectionSummaries = (
|
||||
user: User,
|
||||
user: LocalUser,
|
||||
collections: Collection[],
|
||||
collectionFiles: EnteFile[],
|
||||
) => {
|
||||
@@ -1968,7 +1968,7 @@ const enqueuePendingSearchSuggestionsIfNeeded = (
|
||||
* users who have shared a collection with the user.
|
||||
*/
|
||||
const constructUserIDToEmailMap = (
|
||||
user: User,
|
||||
user: LocalUser,
|
||||
collections: GalleryState["collections"],
|
||||
): Map<number, string> => {
|
||||
const userIDToEmail = new Map<number, string>();
|
||||
@@ -1990,7 +1990,7 @@ const constructUserIDToEmailMap = (
|
||||
* are trying to share albums with specific users.
|
||||
*/
|
||||
const createShareSuggestionEmails = (
|
||||
user: User,
|
||||
user: LocalUser,
|
||||
familyData: FamilyData | undefined,
|
||||
collections: Collection[],
|
||||
): string[] => [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @file Storage (in-memory, local, remote) and update of various settings.
|
||||
*/
|
||||
|
||||
import { partialLocalUser } from "ente-accounts/services/user";
|
||||
import { savedPartialLocalUser } from "ente-accounts/services/accounts-db";
|
||||
import { isDevBuild } from "ente-base/env";
|
||||
import log from "ente-base/log";
|
||||
import { updateShouldDisableCFUploadProxy } from "ente-gallery/services/upload";
|
||||
@@ -195,10 +195,8 @@ const setSettingsSnapshot = (snapshot: Settings) => {
|
||||
*/
|
||||
export const isDevBuildAndUser = () => isDevBuild && isDevUserViaEmail();
|
||||
|
||||
const isDevUserViaEmail = () => {
|
||||
const user = partialLocalUser();
|
||||
return !!user?.email?.endsWith("@ente.io");
|
||||
};
|
||||
const isDevUserViaEmail = () =>
|
||||
!!savedPartialLocalUser()?.email?.endsWith("@ente.io");
|
||||
|
||||
/**
|
||||
* Persist the user's map enabled preference both locally and on remote.
|
||||
|
||||
Reference in New Issue
Block a user